字节笔记本
2026年2月22日
Go 语言并发之 MPG 模型
请记住 Go 并发的真理:Do not communicate by sharing memory; instead, share memory by communicating. 不要以共享内存的方式来通信,相反,要通过通信来共享内存。
Go 语言天生的并发能力众所周知,但 Go 是如何轻松实现构造上万协程的呢?今天我们就来深入探讨 Go 并发的 MPG 模型。
MPG 模型核心概念
M(Machine)
M 代表着一个内核线程,也可以称为一个工作线程。Goroutine 就是跑在 M 之上的。
P(Processor)
P 代表着处理器(Processor),它的主要用途就是用来执行 goroutine 的。一个 P 代表执行一个 Go 代码片段的基础(可以理解为上下文环境),所以它也维护了一个可运行的 goroutine 队列,和自由的 goroutine 队列,里面存储了所有需要它来执行的 goroutine。
G(Goroutine)
G 代表着 goroutine 实际的数据结构(就是你封装的那个方法),并维护着 goroutine 需要的栈、程序计数器以及它所在的 M 等信息。
Scheduler(调度器)
Scheduler 代表着一个调度器,它维护有存储空闲的 M 队列和空闲的 P 队列,可运行的 G 队列,自由的 G 队列以及调度器的一些状态信息等。
多个 Goroutine 并发协作
MPG 模型生动地说明了多个协程的工作形式。其中每一个 gopher(土拨鼠)可以看作一个协程(G),其实对于这些 gopher,还有一个"包工头"的 gopher,他来管理这些工作的 gopher,这个包工头就可以看作一个 Schedule 调度器。
在 MPG 模型中:
- P 正在执行的 Goroutine 处于活跃状态
- 处于待执行状态的 Goroutine 形成队列 run queues(本地队列)
并发调度机制
当一个 P 关联多个 G 时,就会处理 G 的执行顺序,这就是并发。当一个 P 在执行一个协程工作时,其他的会在等待。
当正在执行的协程遇到阻塞情况,例如 IO 操作等,Go 的处理器就会去执行其他的协程。因为对于类似 IO 的操作,处理器不知道你需要多久才能执行结束,所以它不会等你执行完。
非抢占式调度
Go 的协程是非抢占式的,由协程主动交出控制权。也就是说,当发生 IO 操作时,并不是调度器强制切换执行其他的协程,而是当前协程交出了控制权,调度器才去执行其他协程。
Goroutine 可能切换的点
- I/O 操作
- select
- channel 操作
- 等待锁
- `runtime.Gosched()`
这些点是 Go 协程可能切换的地方,但并不是一定切换的。
为什么能轻松构造上万协程?
正是因为是非抢占式的,所以才轻松构造上万的协程。如果是抢占式,那么就会在切换任务时,保存当前的上下文环境。因为当前线程如果正在做一件事,做到一半就被强制停止,这时就必须保存很多信息,避免再次切换回来时任务出错。
总结
- 线程是操作系统层面的多任务
- Go 的协程属于编译器层面的多任务
- Go 有自己的调度器来调度
- 一个协程在哪个线程上是不确定的,由调度器决定
- 多个协程可能在一个或多个线程上运行
原文作者:阿泽Aze 来源:知乎专栏 / 51CTO 博客