Golang 并发的一些我自己才看得懂的总结
Goroutine 与调度器
Goroutine 是绿色线程(Green Thread),运行在 Go 运行时的调度器之上。调度器实现了 M:N 模型,即 M 个系统线程调度 N 个 Goroutine。Goroutine 可以在 syscall 进入阻塞状态时自动出让 CPU,类似于 Java 在进入锁以前自动引入自旋,这实际上是一种抢占式调度(Preemptive Scheduling)。也可以通过 runtime.Gosched() 主动出让 CPU。
调度器还可能无缘由地主动抢占 Goroutine 的时间片(比如已经运行了 10ms)。因为是绿色线程,所以可以很便宜地创造百万 Goroutine。在 Go 1.5 以后,可以通过 GOMAXPROCS 来使用更多的逻辑 CPU(而不是系统进程)来利用多核。主线程不是主线程,主线程也是一个 main goroutine。
Go 关键字基本就等同于 Java 中提交一个 Runnable 到 CompletableFuture 的 CommonPool。在没有 Channel 的帮助时,goroutine 几乎可以等同于一个绿色的守护线程。
Go 1.14+ 基于信号的抢占式调度
在 Go 1.14 之前,Go 的抢占式调度主要基于协作式调度,通过函数调用插入检查点来实现。Go 1.14 引入了基于信号的抢占式调度(Signal-based Preemption),通过向运行时间过长的 Goroutine 发送 SIGURG 信号来强制其暂停。这种机制使得长时间运行的函数(如无限循环)也能被及时抢占,提高了调度器的响应性和公平性。
GMP 调度模型
Go 的调度器采用 GMP 模型:
- G(Goroutine):Go 协程,代表需要执行的代码和栈空间
- M(Machine):系统线程,负责执行 G,与操作系统内核线程 1:1 对应
- P(Processor):逻辑处理器,维护一个本地运行队列,存储可运行的 G
Go 实现了 M:N 的调度(而不是 1:M 的调度),G 通过 P(只看到 P),可以在不同的 M 之间自由切换,这是其他 Green Thread 做不到的(因为其他 Green Thread 本质上还是单进程/系统线程)。G 可以阻塞,M 永远不阻塞。
graph TD
subgraph "GMP 调度模型"
M1[M1<br/>系统线程] -->|持有| P1[P1<br/>逻辑处理器]
M2[M2<br/>系统线程] -->|持有| P2[P2<br/>逻辑处理器]
M3[M3<br/>系统线程] -->|等待| P3[P3<br/>逻辑处理器]
P1 -->|本地队列| G1((G1))
P1 -->|本地队列| G2((G2))
P1 -->|本地队列| G3((G3))
P2 -->|本地队列| G4((G4))
P2 -->|本地队列| G5((G5))
P3 -->|本地队列| G6((G6))
subgraph "全局队列"
G7((G7))
G8((G8))
end
P1 -.->|工作窃取| P2
P2 -.->|工作窃取| P1
P1 -.->|获取| G7
P2 -.->|获取| G8
end
style G1 fill:#90EE90
style G2 fill:#90EE90
style G3 fill:#90EE90
style G4 fill:#90EE90
style G5 fill:#90EE90
style G6 fill:#90EE90
style G7 fill:#87CEEB
style G8 fill:#87CEEB
Goroutine 调度流程
Goroutine 的生命周期包括创建、放入队列、调度执行、阻塞或完成等阶段:
graph LR
A[创建 Goroutine] --> B[放入 P 本地队列<br/>或全局队列]
B --> C{队列是否为空}
C -->|否| D[M 从 P 获取 G]
C -->|是| E[M 从全局队列获取<br/>或工作窃取]
E --> D
D --> F[执行 G]
F --> G{执行状态}
G -->|阻塞| H[将 G 放入等待队列<br/>M 绑定其他 G]
G -->|完成| I[清理 G 资源]
G -->|时间片用完| J[将 G 放回队列<br/>重新调度]
H --> K[阻塞结束<br/>重新入队]
K --> B
J --> B
调度器采用了工作窃取(Work Stealing)算法,当某个 P 的本地队列为空时,可以从其他 P 的本地队列窃取一半的 Goroutine,也可以从全局队列获取。调度器的细节可以查看《也谈goroutine调度器》。
Channel 与 CSP 模型
Channel 是 Go 并发的核心原语,实现了 CSP(Communicating Sequential Processes,通信顺序进程)模型。CSP 的核心理念是 “通过通信来共享内存”(Share memory by communicating),而不是传统的 “通过共享内存来通信”。
Channel 的无 buffer 版本和有 buffer 版本类似于 Java 的 SynchronousQueue 和 BlockingQueue。Channel 是化异步为同步的利器。select 语句中的 default 和 timeout 可以让阻塞操作变为非阻塞的操作。Channel 的阻塞也会引起 Goroutine 的调度。
Channel 通信过程
sequenceDiagram
participant G1 as Goroutine 1<br/>(Sender)
participant Ch as Channel
participant G2 as Goroutine 2<br/>(Receiver)
Note over G1,G2: 无缓冲 Channel
G1->>Ch: ch <- value<br/>(阻塞等待接收者)
Ch->>G2: value<br/>(唤醒接收者)
G1-->>G1: 继续执行
Note over G1,G2: 有缓冲 Channel<br/>(缓冲区未满)
G1->>Ch: ch <- value<br/>(直接写入缓冲区)
G1-->>G1: 继续执行
G2->>Ch: <-ch<br/>(从缓冲区读取)
G2-->>G2: 获取值
Note over G1,G2: 有缓冲 Channel<br/>(缓冲区已满)
G1->>Ch: ch <- value<br/>(阻塞等待空间)
Ch->>G2: 唤醒接收者
G2->>Ch: <-ch<br/>(释放缓冲区空间)
G1->>Ch: value<br/>(写入成功)
G1-->>G1: 继续执行
Channel 的特性
- 单向 Channel:单向 channel 有点像泛型里的单边操作的 wildcard,可以封印掉读/写能力,而且需要通过声明强转。
- Channel 关闭:channel 可以被关闭。关闭有什么用呢?不关闭它也会被垃圾回收,关闭只是 sender 给 receiver 发送的一个状态变化。
- Select 语句:channel 是通过描述若干操作的模式匹配来实现 select 的。
Context 包与 Goroutine 生命周期
Go 的 context 包提供了一种跨 API 边界传递取消信号、截止时间和其他请求范围值的标准方法。它主要用于协调多个 Goroutine 的生命周期,实现优雅的取消和超时控制。
Context 的类型
context.Background():根 context,通常用于主函数、初始化和测试context.TODO():不确定使用什么 context 时使用context.WithCancel():创建可取消的 contextcontext.WithTimeout():创建带超时的 contextcontext.WithDeadline():创建带截止时间的 contextcontext.WithValue():创建携带键值对的 context
Context 使用示例
1 | |
Sync 包的并发工具
Go 的 sync 包提供了多种并发控制工具,虽然 Go 提倡使用 channel 进行通信,但在某些场景下,sync 包的工具更加高效和简洁。
WaitGroup
WaitGroup 用于等待一组 Goroutine 完成:
1 | |
Once
Once 确保某个函数只执行一次:
1 | |
Pool
Pool 是临时对象的缓存池,用于减少内存分配:
1 | |
Mutex
Go 也有 mutex,虽然不提倡使用(提倡用 channel),但在某些场景下仍然有用:
1 | |
与其他并发方案的对比
异步回调
异步回调会把程序拆得七零八落。人脑还是线性思维的。化异步为同步是最重要的。Goroutine 就是化异步为同步的。
GreenThread/Coroutine/Fiber 方案
GreenThread/Coroutine/Fiber 方案遇到阻塞的时候,可以自己 yield 出 CPU,但还是需要外部的线程把 context resume 回来。Go 则由 scheduler 自动识别代劳了。需要外部线程来不断切换 context,其实是一种单线程的并发,尽量减少阻塞时间而能够多利用 CPU 罢了。
M:1 或 M:N 调度
因为有了遇到 IO 阻塞的时候自动 yield 的机制,所以其他机制中 M:1 或者 M:N 的时候,单线程可能会用 IO 系统调用阻塞整个进程的情况,不再发生。
参考资源
更多的例子见 Go by Example、《并发之痛 Thread,Goroutine,Actor》、《Go 调度器: M, P 和 G》 和 《golang大杀器GMP模型》。



