Go 语言的协程调度模型

2025/06/19

Categories: 技术 Tags: Golang

进程、线程和协程

在操作系统中,进程是资源分配的基本单位,线程是 CPU 调度的基本单位,而协程则是用户态的轻量级线程。

协程通过调度器在单个线程中实现多任务并发执行。协程的调度器负责管理协程的生命周期和执行顺序,通常使用协作式调度或抢占式调度。

GPM 调度模型

GPM 调度模型中的 G、P、M 分别代表 Goroutine、Processor 和 Machine,它通过将 Goroutine 映射到 Processor 上,并在 Machine 上执行,实现了高效的并发执行。

Goroutine 的轻量级特性和协作式调度使得 Go 语言能够轻松处理数以万计的并发任务。GPM 模型的设计理念和实现方式为 Go 语言的高性能并发编程提供了强大的支持。

「图片来自 TonyBai

底层数据结构

go1.23 对应结构体定义如下,省略部分字段和方法,完整源代码见 Github

type g struct {
    stack       stack   // offset known to runtime/cgo
    stackguard0 uintptr // offset known to liblink
    stackguard1 uintptr // offset known to liblink

    _panic *_panic // innermost panic - offset known to liblink
    _defer *_defer // innermost defer
    m      *m      // current m; offset known to arm liblink
    sched  gobuf

    goid uint64

    // 省略其他字段 ...
}

type m struct {
    g0   *g // Goroutine with scheduling stack
    curg *g // current running Goroutine

    p     puintptr // attached p for executing go code (nil if not executing go code)
    nextp puintptr
    oldp  puintptr // the p that was attached before executing a syscall

    // 省略其他字段 ...
}

type p struct {
    m muintptr // back-link to associated m (nil if idle)

    // Queue of runnable Goroutines. Accessed without lock.
    runqhead uint32
    runqtail uint32
    runq     [256]guintptr
    runnext  guintptr

    // 省略其他字段 ...
}

type schedt struct {
    midle        muintptr // idle m's waiting for work
    nmidle       int32    // number of idle m's waiting for work
    nmidlelocked int32    // number of locked m's waiting for work
    mnext        int64    // number of m's that have been created and next M ID
    maxmcount    int32    // maximum number of m's allowed (or die)
    nmsys        int32    // number of system m's not counted for deadlock
    nmfreed      int64    // cumulative number of freed m's

    // Global runnable queue.
    runq     gQueue
    runqsize int32

    // 省略其他字段 ...
}

G(Goroutine)

Goroutine 是 Go 语言的轻量级线程,具有以下特点:

P(Processor)

P 代表处理器,是 Go 语言调度器的核心组件。每个 P 都有一个本地运行队列,用于存储待执行的 Goroutine。

P 的主要职责是:

M(Machine)

M 代表机器,是实际执行 Goroutine 的操作系统线程,每个 M 都有自己的栈和寄存器状态。

M 里面存了两个比较重要的东西:

由于 P 负责 M 与 G 的关联,所以 M 中还存储了与 P 相关的数据:

最多会有 GOMAXPROCS 个活跃线程能够正常运行,默认情况下 GOMAXPROCS 被设置为内核数,假如有四个内核,那么默认就创建四个线程,每一个线程对应一个 runtime.m 结构体。线程数等于 CPU 内核个数的原因是,每个线程分配到一个 CPU 上就不至于出现线程的上下文切换,可以保证系统开销降到最低。

工作原理

GPM 模型的工作原理可以概括为以下几个步骤:

  1. 通过关键字创建一个新的 G 时,调度器会将其添加到 P 的本地运行队列中,如果 P 的本地队列已经满了就会保存在全局队列中。
  2. M 会从 P 的本地队列中获取一个可执行状态的 G 来执行,如果 P 的本地队列为空,则尝试从全局队列取一批 G,若全局队列也为空,就会向其他的 M-P 组合偷取一个可执行的 G 来执行。
  3. M 执行分配给它的 G,并在执行过程中进行上下文切换。当 G 阻塞或主动让出 CPU 时,M 会释放绑定的P,把 P 转移给其他空闲的线程执行。
  4. P 会从本地运行队列中获取下一个 G,并将其分配给 M 执行。这个过程会持续进行,直到所有 G 执行完毕。
                            +-------------------- sysmon ---------------//------+
                            |                                                   |
                            |                                                   |
               +---+      +---+-------+                   +--------+          +---+---+
go func() ---> | G | ---> | P | local | <=== balance ===> | global | <--//--- | P | M |
               +---+      +---+-------+                   +--------+          +---+---+
                            |                                 |                 |
                            |      +---+                      |                 |
                            +----> | M | <--- findrunnable ---+--- steal <--//--+
                                   +---+
                                     |
                                   mstart
                                     |
              +--- execute <----- schedule
              |                      |
              |                      |
              +--> G.fn --> goexit --+

关键设计策略

  1. Work Stealing:当 P 的本地运行队列为空时,M 会尝试从其他 P 的本地队列中偷取 Goroutine,这样可以充分利用多核 CPU 的并发能力。
  2. Hand Off: 当 M 因系统调用或其他原因阻塞时,调度器会将 M 的本地运行队列 P 转移到其他空闲的 M 去执行,以避免 Goroutine 的执行被阻塞。
  3. 抢占式调度:当 Goroutine 执行时间过长时,调度器会强制切换到其他 Goroutine,以避免单个 Goroutine 占用过多 CPU 时间。
  4. 全局队列与负载均衡:全局队列作为本地队列的补充,确保所有 Goroutine 都能被执行。调度器会在 P 之间进行负载均衡,以确保每个 P 的本地队列都能保持一定的工作量。

参考文档