Go GMP模型
图中体现了如下模型结构与调度流程:
- 多个 G 会排队在各自绑定的 P 的本地队列中,等待被调度。
- 每个 M 必须绑定一个 P,才能上 CPU 执行对应排队的 G。
- 如果本地队列空,M 会从全局队列或其他 P 的本地队列中“窃取”可运行的 G,保持系统负载均衡。
- 这种 工作窃取 + 本地/全局队列 的机制确保了高效的 Goroutine 调度和多核利用率。
G —- goroutine 可以理解为一个个任务(代码段)
P —- 调度处理器 持有一个任务队列(n个G 类似祖玛游戏里头的珠子 oneByone)(P 的数量由 GOMAXPROCS 决定(通常等于 CPU 核心数))
M —- 真正上CPU运行的内核线程
正常来说一个P会绑定一个M
由于M:N模型中 一个内核线程会在用户空间被映射出多个虚拟线程G(内核线程无法感知)
这样就会出现跟1:N模型一样的问题
当某个G阻塞了线程那么M就会被操作系统挂起,
但是M持有的是一个G队列,即使同队列某个G被阻塞情况下,其他G应该正常被调度。
GO为了解决这个问题,会让阻塞的M和对应的P解绑。然后解绑的P绑定新的M。从而使队列中的其他任务能够正常被调度运行。
窃取机制
当一个 P 的本地队列空闲时,M 会主动从其他 P 或全局队列队尾窃取 G,避免资源浪费,提高并发均衡性。
阻塞处理机制
如果某个 G 阻塞(如进入系统调用),其执行的 M 将与 P 解绑定,P 重新被可用的 M 使用,从而确保其他 G 不受影响继续执行
问题
1. Go 是否保证每个 G 都被 M 完整执行?
- 不会。Go 的运行时是 可抢占(preemptive)的,尤其在 Go 1.14 引入了强制抢占机制。
这意味着一个 G 在执行超过一定时间(通常约 10 毫秒)后,有可能被中断,然后在稍后某个安全点继续执行。
抢占主要由 sysmon(调度器监控线程)触发。sysmon 会向运行中的 M 发送 SIGURG 信号,请求抢占该 Goroutine。
2. 抢占时发生了什么?G 的状态如何保存?
抢占过程中,Go 运行时会准确记录 Goroutine 的执行状态,并在适当的时机恢复执行:
系统会安全地保存当前 G 的上下文,包括寄存器、程序计数器(PC)、栈指针等,这类似于操作系统中的PIB TIB 上下文切换(context switch)。
携带上下文切换的数据可以存储在 G 对象的结构中(如 g struct),或者通过构造一个模拟的栈帧来保存执行点——目的是记录“执行到哪了”,以便恢复时继续运行。
Go 通过两个路径实现抢占:
同步抢占:G 在进入安全点(如函数调用、I/O 阻塞、显式调度调用等)时检查是否有抢占请求。
异步抢占(Go 1.14 起):通过信号直接强制暂停运行中的 G,不依赖 G 自我让出
3. 抢占后,G 会被怎样处理?放哪个队列?
抢占操作不仅要保存状态,还要正确重新排队:
被抢占的 G 会被放入 全局运行队列的尾部,以防它反复被调度,造成其他 G 被饿死。
然后,当其再次有机会执行时,会按照 FIFO 或调度策略,从队列(local 或 global)中取出继续运行。