golang协程核心调度机制是基于 GMP模型。
G是Goroutine
- 是轻量级的、由 Go 运行时 (Go Runtime) 管理的并发执行单元,是golang独有的概念,包含了自己的栈、指令指针和状态。
- 启动时一般只需要2kb的栈空间,创建和销毁成本极低,并且完全由代码控制,而非操作系统,G数量理论上只受内存的影响,使用及其简便。
M是Machine
- 可以理解为操作系统的线程,与物理cpu绑定,真正执行指令的单元。
- golang在运行时,默认会按照cpu核心数启动相应数量的M,而在实际开发中开发者可以通过参数GOMAXPROCS进行控制。
P是Processor,也是context:
- 代表一个逻辑处理器,是CPU内核支持的虚拟处理单元,是M执行G的桥梁,是P获取可运行的G分配给M。
- 它维护了一个本地可运行 Goroutine 队列 。
- P的数量决定了同时可以有多少个 Goroutine 真正并行执行(因为一个 P 需要绑定到一个 M 上才能运行 G)。P 的数量默认等于 GOMAXPROCS 的值。
- 每个 P 有一个本地可运行 Goroutine 队列 (LRQ - Local Runnable Queue)。
理解 GMP 的关键在于它们之间的动态协作关系:
一、基本执行流程:
- 一个 P 必须绑定到一个 M 上才能构成一个有效的执行单元。
- P 从自己的本地可运行队列中 中获取一个状态为 Runnable 的 G。
- M 开始执行这个 G 的代码。
- G 执行完毕后,P 会继续从 LRQ 获取下一个 G 来执行。
二、队列:
- LRQ (Local Runnable Queue): 每个 P 都有一个自己的本地队列,存放待运行的 G。访问 LRQ 不需要加锁(或使用非常轻量级的锁),效率很高。新创建的 G 通常优先放入当前 P 的 LRQ。
- GRQ (Global Runnable Queue): 还有一个全局的可运行 G 队列。当 G 没有明确的 P 归属(例如从网络回调中唤醒)或 P 的 LRQ 满了,G 可能会被放入 GRQ。P 会定期检查 GRQ。访问 GRQ 需要加锁,成本相对较高。
三、创建 Goroutine (go func()): 当执行 go 关键字创建一个新的 Goroutine 时,这个新的 G 对象通常会被优先放入当前 P 的 LRQ。 如果当前 P 的 LRQ 已满,则可能会将当前 LRQ 的一半 G 和这个新的 G 一起放入 GRQ。
四、G 的阻塞 (关键!):
1、系统调用 (Syscall): 当一个 G 执行了一个可能阻塞的系统调用(如文件读写、网络 IO 等):
- G 的状态变为 Waiting。
- 当前 M 会与 P 解绑。
- P 会尝试寻找一个空闲的 M(或者如果 M 数量没达到上限,就创建一个新的 M)来绑定,然后继续执行 LRQ 中的其他 G。
- 执行系统调用的 M 则会阻塞在内核态,等待系统调用完成。
- 系统调用完成时:
-
- 阻塞的 G 会被唤醒,状态变为 Runnable。
-
- 这个 M 会尝试重新获取一个 P。
-
- 如果 M 成功获取到一个 P(可能是它之前解绑的那个 P,也可能是其他的空闲 P),它就会继续执行这个刚唤醒的 G。
-
- 如果 M 获取 P 失败(比如 P 的数量已经达到 GOMAXPROCS 且都被占用了),这个 G 会被放入 GRQ,等待其他 P 来调度。这个 M 可能会进入休眠(被 Park)。
2、Channel 操作 / Mutex 阻塞:
- 当 G 因为读写 Channel 或等待 Mutex 而阻塞时,G 的状态变为 Waiting。
- G 会被移出当前的 M 和 P 的执行上下文,通常会被放入与该 Channel 或 Mutex 相关联的等待队列中。
- P 会立即从 LRQ 中选择下一个 Runnable 的 G,让当前的 M 继续执行,M 不会阻塞,P 也不需要解绑。
- 当 Channel 可读/写或 Mutex 被释放时,等待队列中的 G 会被唤醒,状态变为 Runnable,并被放入某个 P 的 LRQ(通常是唤醒它的那个 G 所在的 P 的 LRQ 或 GRQ),等待调度执行。
五、Work Stealing (工作窃取):
- 当一个 P 的 LRQ 为空时,它不会立刻闲置。
- 它会先尝试从 GRQ 获取 G。
- 如果 GRQ 也为空,它会随机选择另一个 P,并尝试从那个 P 的 LRQ "窃取" 一半的可运行 G 到自己的 LRQ 中来执行。
- Work Stealing 机制极大地提高了调度效率和 CPU 利用率,实现了 Goroutine 在 P 之间的负载均衡。
六、M 的生命周期 (线程管理):
- Go Runtime 会维护一个 M 的池子。
- 当 P 需要 M 但没有空闲 M 时,且 M 数量未达上限,会创建新的 M。
- 当 M 长时间没有 G 可运行(并且没有阻塞在 syscall 中),它可能会被 Runtime 回收或置于休眠状态(Park),以节省系统资源。
- 有专门的系统监控线程 (Sysmon) 负责 M 的创建、休眠、唤醒等管理工作。
七、Sysmon (系统监控线程):
- 这是一个独立于 GMP 模型的、由 Runtime 启动的 M(不占用 P),它在后台运行,执行一些关键的维护任务:
- 垃圾回收 (GC) 触发: 判断是否需要进行 GC。
- 调度器抢占 (Preemption): 检测运行时间过长的 G(防止饿死其他 G),并向其发送信号,使其在安全的点(如函数调用)暂停,让出 M 给其他 G。(这是 Go 1.14 之后引入的基于信号的抢占,使得调度更公平)。
- 网络轮询 (Netpoller): 处理网络 IO 事件,唤醒因网络 IO 而阻塞的 G。
- M 的管理: 回收空闲 M,注入 M 处理阻塞 G 等。
在实际开发中,观察和调整协程的方法:
- 可以通过GOMAXPROCS(n): 设置或获取 P 的数量。通常设置为 CPU 核心数可以获得较好的并行性能
- 通过go tool trace 生成详细的执行追踪文件,可以可视化分析 Goroutine 的调度、阻塞、GC 等。
- 通过runtime/pprof 生成火焰图分析 CPU 使用情况、Goroutine 阻塞情况等,帮助定位性能瓶颈和调度问题