字节笔记本

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 博客

分享: