跳到主要内容

解析 Go 语言的 GMP 并发模型

· 阅读需 3 分钟
素明诚
Full stack development

GMP 并发模型

Goroutine (G)

Goroutine 是 Go 语言中的轻量级线程,每个 Goroutine 代表一个并发执行的任务。它们的创建和销毁成本很低,且运行时系统会自动处理它们的调度和执行。

Machine (M)

Machine 是操作系统级的线程,是真正执行代码的实体。每个 M 都绑定到一个操作系统线程,它执行与之关联的 Goroutine 的代码。

Processor (P)

Processor 是逻辑处理器,它负责调度 Goroutine 到 M 上执行。每个 P 有自己的本地运行队列,存放等待执行的 Goroutine。

GMP 模型的核心工作细节

Goroutine 调度

Go 的运行时系统维护了全局和本地的运行队列。全局运行队列包含所有新创建和被阻塞后重新就绪的 Goroutine。每个 P 的本地运行队列包含了即将由该 P 调度的 Goroutine。只有在本地运行队列装满后(256 个 Goroutine)才会把新来的 Goroutine 放入本地运行队列

当 Goroutine 需要执行系统调用时,它可能会被阻塞。在这种情况下,与 Goroutine 关联的线程(M)会被解除绑定,并可能会为其他未阻塞的 Goroutine 创建一个新的 M。

Go 的运行时系统还管理一个定时器堆,用于处理定时事件。Goroutine 可以创建定时器,而运行时系统将确保在指定的时间后将它们唤醒。

Go 的运行时系统包含一个网络轮询器,它可以在非阻塞模式下检查网络 I/O。如果一个 Goroutine 在等待网络 I/O,它会被放入网络轮询器,直到 I/O 准备好为止。

线程创建和销毁

当所有的 M 都在执行 Goroutine,并且还有更多的 Goroutine 等待执行时,运行时系统可能会创建新的 M。一旦 M 完成了 Goroutine 的执行并且没有更多的 Goroutine 等待执行,M 可能会被销毁或放回线程池以备后用。

例如当线程池中的线程数量超过了一定的阈值或者线程已经存在了很长时间没有被重用时,运行时系统可能会选择销毁 M 以释放资源。

抢占式调度

Go 运行时系统实现了抢占式调度,以确保 Goroutine 公平地获得执行时间。如果一个 Goroutine 长时间占用 CPU,这个时间大概是 10ms,超过 10ms,运行时系统会抢占它,将它放回运行队列,并允许其他 Goroutine 执行。

网络和系统调用

当 Goroutine 阻塞在网络或系统调用时,它的 M 会解除与 P 的绑定,使 P 可以调度和执行其他 Goroutine。一旦系统调用完成,Goroutine 会再次被放入运行队列,等待被调度执行。

垃圾回收的协助

Go 的垃圾回收是并发执行的,它与 GMP 模型紧密集成。Goroutine 会在必要时协助运行时系统进行垃圾回收。

Work stealing

当一个 P 的本地运行队列为空时,它会尝试从其他 P 的本地运行队列中“偷取” Goroutine,以保持 CPU 的利用率,一次偷走一半。