1. Go并发基础
1.1 并发与并行的核心区别
在进入Go的并发世界之前,必须厘清两个最容易被混淆的概念:并发(Concurrency)与并行(Parallelism)。
- 并发:指两个或多个任务在同一时间段内交替执行。在单核CPU上,通过时间片轮转实现“逻辑上同时执行”。
- 并行:指两个或多个任务在同一时刻真正同时执行。这需要多核CPU或多处理器硬件支持。
Go设计哲学:Go通过goroutine实现并发,并在运行时自动将并发任务映射到操作系统线程上,从而在任意核数的机器上都能高效利用并行能力。你只需编写并发逻辑,Go负责并行调度。
| 维度 | 并发 | 并行 |
|---|---|---|
| 执行方式 | 交替执行(时间片轮转) | 同时执行(多核并行) |
| 硬件依赖 | 单核即可实现 | 必须多核/多CPU |
| 关注点 | 任务结构设计(如何拆解) | 执行效率(如何加速) |
| Go中的体现 | goroutine + channel 编排 | GOMAXPROCS 控制并行度 |
记忆口诀:并发是结构,并行是执行。并发让你能写出清晰的异步代码,并行让代码跑得更快。
1.2 Goroutine vs 操作系统线程
Goroutine是Go语言特有的轻量级“用户态线程”,由Go运行时(runtime)管理,而非操作系统内核。它与传统OS线程有着本质区别:
| 对比项 | OS线程(内核级) | Goroutine(用户级) |
|---|---|---|
| 创建成本 | 高(约1~8MB栈空间) | 极低(初始2~4KB,可动态扩缩) |
| 切换开销 | 高(内核态上下文切换,约1~10μs) | 低(用户态切换,约几十ns) |
| 调度者 | 操作系统内核 | Go运行时(GMP模型) |
| 数量上限 | 数千(受系统资源限制) | 数百万(内存足够即可) |
| 标识符 | 系统级TID | 无全局ID(通过channel/context通信) |
| 通信方式 | 共享内存 + 锁 | 推荐channel(CSP模型) |
避坑提示:不要将goroutine当作“免费”资源无限创建。虽然单个goroutine栈很小,但百万级goroutine仍会消耗大量内存(约4GB+),且GC压力剧增。合理控制goroutine数量是调优的第一步。
1.3 GMP调度模型概述
Go运行时实现了一套高效的GMP调度模型,用于将海量goroutine公平、高效地映射到少量OS线程上执行。GMP是Go并发性能的核心基石。
1.4 GMP调度核心流程
GMP调度模型的核心思想是:M必须绑定P才能执行G。P的数量决定了系统真正的并行度(即同时运行的goroutine数量)。
- G创建:通过
go func()创建一个G,初始状态为_Grunnable,放入当前P的本地队列(或全局队列)。 - G调度:P从本地队列头部取出一个G,绑定到当前M上执行,G状态变为
_Grunning。 - G阻塞:若G发生系统调用(如文件读写、网络IO),M与P解绑,G进入
_Gsyscall状态;P寻找空闲M继续执行其他G。 - G让出:G主动让出(channel阻塞、time.Sleep、runtime.Gosched()),G进入
_Gwaiting状态,P调度下一个G。 - G结束:G执行完毕,进入
_Gdead状态,G的栈被回收或复用。
关键调优参数:GOMAXPROCS 控制P的数量,默认等于CPU逻辑核数。在IO密集型场景,适当增加 GOMAXPROCS 可提高吞吐;在CPU密集型场景,保持默认或略低于核数可减少上下文切换。
1.5 Goroutine与线程的调度对比
为了更直观地理解goroutine的轻量优势,下面从调度机制角度进行对比:
1.6 实战代码示例:启动与观察Goroutine
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
// 获取当前GOMAXPROCS值
fmt.Printf("默认 GOMAXPROCS = %d\n", runtime.GOMAXPROCS(0))
// 启动3个goroutine
for i := 0; i < 3; i++ {
go func(id int) {
for j := 0; j < 5; j++ {
fmt.Printf("Goroutine %d: 第 %d 次执行\n", id, j)
time.Sleep(10 * time.Millisecond) // 主动让出CPU
}
}(i)
}
// 让主goroutine等待,否则程序直接退出
time.Sleep(100 * time.Millisecond)
fmt.Println("主goroutine结束")
}
运行观察:执行上述代码,你会看到3个goroutine交替打印输出,但它们的执行顺序并不固定。这是因为Go调度器在 time.Sleep 处让出CPU,P会调度其他就绪的G继续执行。
1.7 本章小结
- 并发 ≠ 并行:并发是结构设计,并行是执行效率。Go通过goroutine实现并发,通过GMP模型实现并行。
- Goroutine轻量本质:用户态调度、动态栈、协作式抢占,使得百万级goroutine成为可能。
- GMP三要素:G(任务)、M(执行者)、P(调度器)。P的数量决定并行度,由
GOMAXPROCS控制。 - 调优起点:理解GMP模型是后续goroutine数量调优、内存模型分析的基础。