GoLang 的协程调度和 GMP 模型

本文深入探讨GoLang的GMP模型,解析GoLang如何利用G(协程)、M(机器)和P(处理器)实现高效并发。文章详细介绍了GoLang的启动过程,GMP模型的各组件功能,以及关键函数的运作机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

GoLang 的协程调度和 GMP 模型

在这里插入图片描述

GoLang 是怎么启动的

关于 GoLang 的汇编语言,请查阅 参考文献[1]参考文献[2]

  1. 编写一个简单的 GoLang 程序 main.go, 用 go build -o main main.go 编译生成可执行文件 main

    package main
    import "fmt"
    func main(){
        fmt.Println("Hello, World!")
    }
    
  2. 使用 gdb main 命令调试 main 程序

  3. 输入 info files 命令查看程序入口地址
    entry point

  4. break 命令给该地址打断点,然后运行程序。发现程序在 _rt0_amd64_linux 函数的 JMP _rt0_amd64(SB) 命令停下。
    在这里插入图片描述

  5. _rt0_amd64_linux 就是入口函数,该函数没有做任何操作直接跳转到 _rt0_amd64(SB),见 参考文献[6]
    在这里插入图片描述

  6. 继续跳转到 _rt0_amd64, 可以看到该函数将存放在栈内的 argcargv 复制给参数寄存器 DISI 后又跳转到 runtime·rt0_go(SB)
    在这里插入图片描述

  7. 参考文献[8] 中我们可以看到 GoLang 跑起来的主要代码:
    在这里插入图片描述
    (1) 使用 schedinit 始化堆栈,GC 等,根据 GOMAXPROCS 设置创建 P 结构的池子。参考文献[10]
    (2) 创建一个新的 G,来执行 runtime.mainPC 函数
    (3) 启动当前的 M, mstart 调用 schedule,开始执行之前绑定的 mainPC 所在的 G

GoLang 的 GMP 模型

上一节提到来 GMP 到底是什么意思呢?

  • P(Processor): “处理器”,主要用来限制实际运行的 M 的数量。默认数量跟 CPU 的物理线程数一致,受 GOMAXPROCS 控制。 参考文献[15]

  • M(Machine): OS Thread,由 OS 调度。M 的数量不一定。但是处于非阻塞状态的 MP 决定。

    MP 的区别与联系在于:P 是 GoLang 假想的“处理器”,控制实际能够跑起来的 M 的数量。

    如,在 G 的数量无限多的情况下,一开始 MP 的数量一样多。但是当运行在 M 上的 G 调用了同步系统调用阻塞了 M, 此时这个 M 线程是没办法做其他操作的。

    但是 GoLang 想要运行中的 M(OS Thread) 数量是和 P 的数量一样多的,所以它会再创建一个 M(OS Thread) 来跑新的 G。这样就能动态的保证“运行中的 M 的数量等于 P 的总数”

  • G(GoRoutine):协程,应用层看到的“线程”。由 M 调度和执行。

  • LRQ(Local Run Queue): 从定义可知,P 相当于是对 M 的约束,M 只有绑定来 P 才能实际调度和执行 G

    因此,GoLang 给每个 P 设置了一个 LRQ 来维护一个待执行 G 的队列。

    当有 M 绑定在 P 上时,M 就会优先调度和执行该 PLRQ 上的 G

    需要重点关注的一点是:M 才是 OS Thread,因此只有 M 才有执行和调度 G 的能力。而 P 只是一个约束条件。

  • GRQ(Global Run Queue): 没有绑定任何 PG 就会被扔进 GRQ,等待被需要的 P-M 加入 LRQ 执行。

P 的状态

  • Pidle: means a P is not being used to run user code or the scheduler. Typically, it’s on the idle P list and available to the scheduler
  • Prunning: means a P is owned by an M and is being used to run user code or the scheduler.
  • Psyscall: means a P is not running user code, may be stolen by another M.
  • Pgcstop: means a P is halted for STW and owned by the M that stopped the world.
  • Pdead: means a P is no longer used (GOMAXPROCS shrank). We reuse Ps if GOMAXPROCS increases.

G 的状态:

  • Gidle:just allocated and has not yet been initialized
  • Grunnable:this goroutine is on a run queue
  • Grunning:means this goroutine may execute user code
  • Gsyscall: means this goroutine is executing a system call
  • Gwaiting: means this goroutine is blocked in the runtime(channel)

GMP 的协作

  1. 一般调度:如图,当 M 绑定 P 时,就会开始调度执行 PLRQ 中的 G

    当执行 61 ticks,或者 LRQ 没有 G 时,就会去 GRQ 或者其他的 PLRQG 来执行。

    需要特别注意的是图中 LRQGMPCore 的相对位置关系。M 需要绑定 P 才能调度执行 G, 因此 G 在它俩之间。
    在这里插入图片描述

  2. 异步系统调用:当 G 需要执行异步系统调用时,M 会把它扔给 NetPoller 管理,然后从 LRQ 中重新取一个 G 来处理。

    NetPoller 通过 epoll 等函数,由监控线程 sysmon 定期询问。当某个 G 的异步系统调用完成后,该 G 就会被扔进 GRQ 中,等待被再次调度。
    在这里插入图片描述

  3. 同步系统调用:我们知道,同步系统调用阻塞了 M(OS Thread), 该 M 是没有办法为其他 G 提供服务的。此时,该 MP 而言就是没有意义的。(P 的意义是约束运行中的 M 的个数)

    所以,GoLang 会将阻塞的 M - GP 解绑,然后给 P 创建/调度一个新的 M。这样就能保证“运行中的 M 的数量等于 P 的总数”。

    当同步系统调用完成后,阻塞的 M 会将执行系统调用的 G 还回 LRQ。然后该 M 等待销毁或者被重新绑定给某个 P
    在这里插入图片描述

几个重要的函数

  1. runtime.schedule 参考文献 [24]
    (1)从 TLS 获取当前正在运行的 G 的信息
    (2)M 是否绑定到当前的 G(同步系统调用)?M 让出绑定的 P ,等待同步系统调用的 G 结束
    (3)GC?STW(stop the word) for GC
    (4)当前 P 每执行 61 ticks,从 GRQ 取一定量的 G 加入 LRQ
    (5)从 LRQ 获取可执行的 G
    (6)如果当前的 LRQ 没有 G, 则从 GRQ 或者其他 PLRQG 来调度执行
    (7)如果都没有找到 G,当前 M 让出占用的 P, 进入休眠状态

  2. runtime.mainPC(runtime.main) 参考文献[20]
    在这里插入图片描述
    (1)限制最大栈大小: Max stack size is 1 GB on 64-bit, 250 MB on 32-bit
    (2)创建一个不需要绑定 PM,,执行 sysmon 函数
    (3)创建 GC goroutine,启动 GC
    (4)运行 package mainmain 函数
    (5)一系列的收尾操作

  3. runtime.sysmon 参考文献[21]
    (1)获取 NetPoller 中已完成操作的 G,将其加入 GRQ
    (2)retake

    • 抢占长时间运行的 G
    • 回收被 syscall 长时间阻塞的 P

参考文献

### GolangGMP 调度模型的工作原理细节 #### 什么是 GMP 模型 Golang调度器采用了一种名为 GMP模型来管理任务的执行。该模型通过三种核心组件协同工作,分别是: - **G(Goroutine)**: 表示轻量级的任务单元,类似于协程。 - **M(Machine 或 OS Thread)**: 表示操作系统级别的线程。 - **P(Processor)**: 表示逻辑处理器,用于协调 G M 的关系[^1]。 这种设计使得 Go 可以高效地利用多核 CPU 并发执行多个 Goroutine,同时减少了传统线程切换带来的开销。 #### 组件间的关系与交互 ##### 1. G(Goroutine) G 是用户编写的代码中最基本的执行单位。每一个 Goroutine 都有一个独立的栈空间,默认初始大小较小(几 KB),可以根据需要动态扩展。相比传统的线程,它更加节省内存资源,并且可以轻松创建数百万个实例而不至于耗尽系统资源[^3]。 ##### 2. M(OS Thread) M 实际上就是操作系统提供给应用程序的标准线程。每个 M 至少绑定一个 P 来运行一组 Gs。如果没有足够的可用 Ps,则新启动的 Ms 将处于等待状态直到有空闲 P 分配过来。 ##### 3. P(Processor) P 主要承担了两项职责:一是持有若干待处理的 G;二是充当连接 G M 的桥梁。每个 P 维护了一个本地队列用来暂存即将被执行的 Gs。此外还存在全局共享队列以及 work-stealing 机制以便于跨不同 Processor 之间平衡负载[^2]。 #### 关键流程解析 下面详细介绍几个重要的操作环节: ##### (1)初始化阶段 当程序刚开始运行时,会预先设定一定数量的 P(默认等于物理 CPU 数目)。接着每一对组合好的 MP 对象都会进入循环准备接受来自各自所属 P 的指令去激活相应的 G 进行实际运算[^3]。 ##### (2)任务分配与执行 一旦某个特定时刻某条路径上的所有条件都满足后就会触发如下动作序列: - 当前活动中的 Goroutine 结束了自己的生命周期; - 系统自动跳转回至特殊保留下来的零号 Goroutine(G0) 上面继续后续管理工作; - Scheduler 函数依据既定规则挑选下一个合适的候选目标出来加载到指定位置重新开启新一轮迭代过程[^3]。 ```go func schedule() { gp := getg() // Switch to the scheduler context. casgstatus(gp, _Grunning, _Gsyscall) mcall(execute) } ``` 这里展示了部分简化版伪码片段说明了如何完成上下文转换的具体方法论之一例证形式呈现给大家参考学习之用[^4]。 ##### (3)抢占式调度引入后的改进措施 早期版本中主要依靠开发者主动配合才能达到预期效果的方式被称为合作型模式(cooperative scheduling),但这种方式容易受到某些极端情况的影响从而引发不公平现象甚至死锁等问题的发生几率增加不少。自 Go 1.14 起改用了基于单一信号量(single signal-based preemption mechanism)的新方案有效解决了上述难题的同时也进一步提升了整体稳定性可靠性水平[^4]。 --- ###
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值