被废弃的golang调度器
在2012年之前,用的是P不存在的GM调度器
线程M 想要执行、放回 协程G 都必须访问全局 G 队列,并且 M 有多个,即多线程访问同一资源需要加锁进行保证互斥 / 同步,所以全局 G 队列是有互斥锁进行保护的。
这样明显会发现一个问题,当M空闲都申请G时,形成锁竞争;甚至M0申请G、M1放回G都会形成竞争,效率低下。
老调度器有几个缺点:
-
创建、销毁、调度 G 都需要每个 M 获取锁,这就形成了激烈的锁竞争。
-
M 转移 G 会造成延迟和额外的系统负载。比如当 G 中包含创建新协程的时候,M 创建了 G’,为了继续执行 G,需要把 G’交给 M’执行,也造成了很差的局部性,因为 G’和 G 是相关的,最好放在 M 上执行,而不是其他 M’。
-
系统调用 (CPU 在 M 之间的切换) 导致频繁的线程阻塞和取消阻塞操作增加了系统开销。
GMP的设计思想
在 Go 中,线程是运行 goroutine 的实体,调度器的功能是把可运行的 goroutine 分配到工作线程上。
- 全局队列(Global Queue):存放等待运行的 G。
- P 的本地队列:同全局队列类似,存放的也是等待运行的 G,存的数量有限,不超过 256 个。新建 G’时,G’优先加入到 P 的本地队列,如果队列满了,则会把本地队列中一半的 G 移动到全局队列。
- P 列表:所有的 P 都在程序启动时创建,并保存在数组中,最多有 GOMAXPROCS(可配置) 个。
- M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,P 队列为空时,M 也会尝试从全局队列拿一批 G 放到 P 的本地队列,或从其他 P 的本地队列偷一半放到自己 P 的本地队列。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。
Goroutine 调度器和 OS 调度器是通过 M 结合起来的,每个 M 都代表了 1 个内核线程,OS 调度器负责把内核线程分配到 CPU 的核上执行。
P和M的数量如何确定?
P 的数量由启动时环境变量 GOMAXPROCS
或者是由 runtime 的方法GOMAXPROCS()
决定。这意味着在程序执行的任意时刻都只有 GOMAXPROCS
个 goroutine 在同时运行。
M 的数量受go 语言本身的限制:go 程序启动时,会设置 M 的最大数量,默认 10000. 但是内核很难支持这么多的线程数,所以这个限制可以忽略。
程序员可以通过runtime/debug 中的 SetMaxThreads
函数,设置 M 的最大数量。
并且一个 M 阻塞了,会创建新的 M。
另外,M 与 P 的数量没有绝对关系,一个 M 阻塞,P 就会去创建或者切换另一个 M,所以,即使 P 的默认数量是 1,也有可能会创建很多个 M 出来。
G的调度过程
Go 语言中 goroutine
(简称 G
)的调度过程如下:
创建阶段
- 当使用
go
关键字创建一个goroutine
时,新的G诞生。此时有两种情况:- Case1:若本地队列(与处理器
P
关联 )未满,G
加入到本地队列。 - Case2:若本地队列已满,
G
加入到全局队列。
- Case1:若本地队列(与处理器
调度执行阶段
- 处理器(P)与机器线程(M)协作:
- 每个
P
会绑定一个M
(M
代表操作系统线程) 。M
从P
的本地队列获取G
来执行。 - 当
M1
的本地队列空时,会从全局队列获取G
。 - 若全局队列也为空,
M
会尝试从其他P
的本地队列中窃取一半的G
(工作窃取算法 )。
- 每个
- 执行过程:
M
拿到G
后开始执行G
关联的函数(G.func()
) 。- 执行过程中,如果
G.func()
发生systemCall
(系统调用)或阻塞: - 当前阻塞
G
的P
会被接管,M
会出列(或新建一个M
)。 - 当阻塞解除,对应的
G
会重新回到队列等待调度执行。 - 当 M 系统调用结束时候,这个 G 会尝试获取一个空闲的 P 执行,并放入到这个 P 的本地队列。如果获取不到 P,那么这个线程 M 变成休眠状态, 加入到空闲线程中,然后这个 G 会被放入全局队列中。
- 调度返回与销毁:
G
执行完G.func()
函数后,经过调度返回,最终被销毁。 而空闲的M
会进入休眠的M
队列等待再次被使用。
G的状态转换
goroutines在其生命周期中会经历几种状态:
- 可运行(Runnable):G已经准备好执行,但还没有被分配到M上。
- 运行中(Running):G正在M上执行。
- 休眠(Waiting):G在等待某些事件(如I/O操作、channel通信或定时器)。
- 死亡(Dead):G的执行已经完成,或者被显式地终止。
在执行期间,goroutine 可能会因为多种原因进入阻塞(休眠)状态,像进行系统调用、等待 I/O 操作完成、获取锁或者等待通道操作等。一旦阻塞的原因消除,goroutine 就会被唤醒,重新回到就绪状态,等待再次被调度执行。
常见的阻塞有
I/O
Select
Block on syscall
Channel
Mutex
Sleep
runtime.Gosched()
tips
非抢占式到抢占式的转变:从 Go 1.14 版本开始,goroutine 实现了基于信号的抢占式调度,避免了之前因某些代码结构(如死循环)导致的非抢占问题。
goroutine 结束后,它所占用的栈空间等资源会被自动释放,这一过程无需手动干预。
挂起机制(Hand off): 当
G
由于系统调用而阻塞时,M
会释放绑定的P
供其他M
使用
调度器的生命周期
为什么一定是M0和G0先被创建?而不是其他的M1、M2?
特殊的M0 和 G0
1. M0(Machine 0)
- 作用:M0 是 Go 程序启动时创建的第一个 操作系统线程(OS Thread),负责执行 Go 的运行时初始化和调度任务。
- 特点:
- 由 Go 运行时在
runtime·mstart
阶段创建,不依赖 Go 的调度器(因为调度器还没初始化)。 - 运行在 主线程(main thread) 上,通常对应程序的初始线程(如 C 语言的
main
线程)。 - 在初始化完成后,M0 会绑定一个 P(Processor),并开始调度 goroutine。
- 如果程序没有启用
GOMAXPROCS > 1
,M0 可能是唯一的 M(操作系统线程)。
- 由 Go 运行时在
2. G0(Goroutine 0)
- 作用:G0 是每个 M 的 专属调度协程,负责管理该 M 上的 goroutine 调度(如创建、切换、销毁 goroutine)。
- 特点:
- 每个 M(包括 M0)都有一个自己的 G0,不归 Go 调度器管理,而是由运行时直接控制。
- G0 的栈是 固定大小(通常约 8KB),用于执行调度、垃圾回收(GC)、栈扩容等低层任务。
- 当 M 需要切换 goroutine 时(如发生系统调用、channel 阻塞等),会先切回 G0,由 G0 决定下一个要运行的 G。
M0 和 G0 的关系
- 程序启动时:
- Go 运行时先创建 M0(主线程)。
- 然后为 M0 创建 G0(调度协程)。
- M0 和 G0 绑定后,Go 运行时开始初始化调度器(
schedinit
)。 - 接着创建
main
goroutine(用户的main()
函数),并将其加入调度队列。
- 运行时调度:
- M0 通过 G0 调度
main
goroutine 并执行用户代码。 - 如果
main
goroutine 结束,G0 会检查是否有其他 goroutine 可运行,如果没有,程序退出。
- M0 通过 G0 调度
概念 | 作用 | 是否归调度器管理 | 栈大小 |
---|---|---|---|
M0 | 第一个 OS 线程,负责初始化 | ❌(直接由运行时管理) | 系统线程栈(通常 2MB) |
G0 | 每个 M 的专属调度协程 | ❌(运行时直接控制) | 固定(约 8KB) |
main goroutine | 执行用户的 main() 函数 |
✅(由调度器管理) | 动态增长(初始 2KB) |
实际应用场景
- 当你的 Go 程序启动时,
runtime
会先运行在 M0 的 G0 上,初始化完成后才切换到main
goroutine。 - 如果 M0 的 G0 崩溃(如栈溢出),整个程序会 panic,因为它是运行时的核心调度器。
GMP在源码中是怎样的
G M P 结构体定义于src/runtime/runtime2.go,本文版本为1.24.3
G的状态
在代码中体现为(https://github.com/golang/go/blob/master/src/runtime/runtime2.go)
const ( //源代码17行
// G 状态
//
// 除了表示 goroutine 的一般状态外,G 状态还像一个锁,用于保护 goroutine 的栈(从而影响其执行用户代码的能力)。
// TODO(austin): _Gscan 标志位可以设计得更轻量级。
// 例如,我们可以选择不运行在运行队列中找到的 _Gscanrunnable 状态的 goroutine,而不是通过比较并交换(CAS)循环直到它们变为 _Grunnable 状态。
// 像 _Gscanwaiting -> _Gscanrunnable 这样的状态转换实际上是可以的,因为它们不影响栈的所有权。
// _Gidle 表示这个 goroutine 刚被分配,还未初始化。
_Gidle = iota // 0
// _Grunnable 表示这个 goroutine 在运行队列中。它当前没有执行用户代码,栈未被占用。
_Grunnable // 1
// _Grunning 表示这个 goroutine 可以执行用户代码。栈由这个 goroutine 占用,它不在运行队列中,并且已分配了一个 M 和一个 P(g.m 和 g.m.p 有效)。
_Grunning // 2
// _Gsyscall 表示这个 goroutine 正在执行系统调用。它没有执行用户代码,栈由这个 goroutine 占用,它不在运行队列中,并且已分配了一个 M。
_Gsyscall // 3
// _Gwaiting 表示这个 goroutine 在运行时被阻塞。它没有执行用户代码,不在运行队列中,但应该在某个地方记录(例如,通道等待队列),以便在必要时可以将其置为就绪状态。
// 栈通常不被占用,但在适当的通道锁下,通道操作可能会读写栈的部分内容。
// 否则,在 goroutine 进入 _Gwaiting 状态后访问栈是不安全的(例如,栈可能会被移动)。
_Gwaiting // 4
// _Gmoribund_unused 目前未使用,但在 gdb 脚本中被硬编码。
_Gmoribund_unused // 5
// _Gdead 表示这个 goroutine 当前未被使用。它可能刚退出,在空闲列表中,或者刚被初始化。
// 它没有执行用户代码,可能分配了栈,也可能没有。G 及其栈(如果有)由退出该 G 或从空闲列表中获取该 G 的 M 拥有。
_Gdead // 6
// _Genqueue_unused 目前未使用。
_Genqueue_unused // 7
// _Gcopystack 表示这个 goroutine 的栈正在被移动。它没有执行用户代码,也不在运行队列中。栈由将其置为 _Gcopystack 状态的 goroutine 占用。
_Gcopystack // 8
......
看似状态很多,其实主要还是图上几个
- 等待中:_ Gwaiting、_Gsyscall 和 _Gpreempted,这几个状态表示G没有在执行;
- 可运行:_Grunnable,表示G已经准备就绪,可以在线程运行;
- 运行中:_Grunning,表示G正在运行;
字段 | 编号 | 描述 |
---|---|---|
_Gidle | 0 | 表示此 Goroutine 刚刚分配并且尚未初始化。 |
_Grunnable | 1 | 表示此 Goroutine 在运行队列中。它当前不执行用户代码。堆栈不属于它。 |
_Grunning | 2 | 表示此 Goroutine 可能执行用户代码。堆栈由此 Goroutine 拥有。它不在运行队列中。它被分配给一个 M 和一个 P。 |
_Gsyscall | 3 | 表示此 Goroutine 正在执行系统调用。它不执行用户代码。堆栈由此 Goroutine 拥有。它不在运行队列中。它被分配给一个 M。 |
_Gwaiting | 4 | 表示此 Goroutine 在运行时被阻塞。它不执行用户代码。它不在运行队列中,但应该在某个地方记录(例如,一个通道等待队列),以便在必要时可以准备就绪。堆栈不属于它,除非在适当的通道锁下,通道操作可能读取或写入堆栈的某些部分。否则,在 Goroutine 进入 _Gwaiting 后访问堆栈是不安全的(例如,它可能会被移动)。 |
_Gmoribund_unused | 5 | 目前未使用,但在 gdb 脚本中硬编码。 |
_Gdead | 6 | 表示此 Goroutine 当前未使用。它可能刚刚退出,位于空闲列表上,或者刚刚初始化。它不执行用户代码。它可能具有堆栈,也可能没有。G 和其堆栈(如果有)由正在退出 G 的 M 或从空闲列表中获取 G 的 M 拥有。 |
_Genqueue_unused | 7 | 目前未使用。 |
_Gcopystack | 8 | 表示此 Goroutine 的堆栈正在移动。它不执行用户代码,也不在运行队列中。堆栈由将其放入 _Gcopystack 的 Goroutine 拥有。 |
_Gscan | 0x1000 | 与除 _Grunning 之外的其他状态组合表示 GC 正在扫描堆栈。Goroutine 不执行用户代码,堆栈由设置 _Gscan 位的 Goroutine 拥有。 |
G结构体中重要字段
源码305行左右
字段 | 说明 |
---|---|
goid | Goroutine ID, 唯一标识符 |
status | Goroutine 状态, 如运行和阻塞 |
stack | Goroutine 栈空间 |
gopc | Goroutine PC 寄存器 |
m | Goroutine 所在的 M |
locked | Goroutine 是否被锁定 |
sched | Goroutine 调度器 |
atomicstatus | Goroutine 原子状态 |
P的状态
const (
// P 状态
_Pidle = iota // 0
_Prunning // 1
_Psyscall // 2
_Pgcstop // 3
_Pdead // 4
)
状态 | 描述 | 关键特性 |
---|---|---|
_Pidle | P 未被用于运行用户代码或调度器,通常在空闲列表中,可能正在状态转换中。 | - 由空闲列表或状态转换中的对象拥有 - 运行队列为空 |
_Prunning | P 由一个 M 拥有,用于运行用户代码或调度器,仅拥有此 P 的 M 可更改其状态。 | - 可转换为_Pidle(无工作时)、_Psyscall(系统调用时)或_Pgcstop(GC 时) - M 可直接转移 P 的所有权 |
_Psyscall | P 未运行用户代码,与系统调用中的 M 有亲和性,但不被 M 拥有,可能被其他 M 抢占。 | - 类似_Pidle 但转换轻量级并保持 M 亲和性 - 离开此状态需使用 CAS 操作,存在 ABA 风险 |
_Pgcstop | P 为 STW(Stop The World)停止,由停止世界的 M 拥有,保留运行队列。 | - 从_Prunning 转换到此状态会使 M 释放 P 并休眠 - startTheWorld 会重启有非空队列的 P |
_Pdead | P 不再使用(GOMAXPROCS 减少),资源被剥离但保留部分内容(如跟踪缓冲区),可被重用。 | - GOMAXPROCS 增加时可重用 |
P结构体重要字段
字段 | 类型 | 描述 |
---|---|---|
id | int32 | P 的唯一标识符 |
status | uint32 | P 的状态 |
link | puintptr | 下一个 P,在 P 链表中 |
m | muintptr | 拥有这个 P 的 M |
mcache | *mcache | 用于内存分配的 P 本地缓存 |
runqhead | uint32 | P 本地 runnable 状态的 G 队列头部,无锁访问 |
runqtail | uint32 | P 本地 runnable 状态的 G 队列尾部,无锁访问 |
runq | [256]guintptr | P 本地 runnable 状态的 G 队列,最多 256 个元素 |
runnext | guintptr | 一个比 runq 优先级更高的 runnable G |
gFree | struct | 状态为 dead 的 G 链表,在获取 G 时会从这里面获取。 |
gcBgMarkWorker | guintptr | (原子操作) 用于 GC 后台标记的 worker |
gcw | gcWork | 用于 GC 的工作结构体。 |