news 2026/6/11 9:22:38

GMP模型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
GMP模型

Go 调度器用 G/M/P 模型。
G 是 goroutine,M 是 OS 线程,P 是执行 Go 代码所需的调度上下文。
M 必须绑定 P 才能运行 G。
P 有本地队列,减少全局锁竞争;找不到 G 时会从全局队列、netpoller 或其他 P 偷取。

  • G 阻塞在 channel/锁时,M/P 可以去执行其他 G
  • G 阻塞在 系统调用 时,M 和 G 一起进入 系统调用,P 会被释放给其他 M

runtime 通过 sysmon 和抢占机制避免 G 长时间占用 P。
GOMAXPROCS 决定 P 数量,也就是 Go 代码最大并行度。

为什么引入协程?

一句话:进程线程太重了,需要用户态和内核态来回切换,协程是用户态切换,更加轻量

理解GMP原理,要先知道G、M、P是什么东西,它们作用是啥?

G 是任务,M 是线程,P 是调度执行 Go 代码所需的“令牌/上下文”。

  • M:线程
  • G:协程,代表任务
  • P:代表逻辑处理器,或者说调度上下文

    P 不是 CPU,也不是线程。它更像是 Go runtime 里的“执行许可证 + 调度缓存”。

    为什么 P 里有 G 队列?因为 Go 调度器不想所有 goroutine 都去抢一个全局队列。那样锁竞争会很重。所以每个 P 都有自己的本地 run queue。这样绑定到这个 P 的 M 要找活干时,优先从自己的 P 本地队列取 G,速度快、锁少。

GOMAXPROCS 控制 P 的数量(P 的数量决定同一时刻最多有多少个 M 并行执行 Go 代码)不是控制 goroutine 数量,也不是严格控制线程总数。M 数量可以大于 P,比如系统调用阻塞时可能创建更多 M。

一个G要执行,需要怎么办?

  1. M 先 绑定 P(P中有G的调度上下文)
  2. 然后,M再拿到 G,M执行G

举例子:

  • M1 绑定 P1,运行 G1
  • M2 绑定 P2,运行 G2
  • M3 没有 P,只能阻塞/休眠,不能执行 Go 代码

没有 P 的 M 不能执行普通 Go 代码;没有 M 的 P 也不能真正运行;G 只有被 M 在某个 P 上调度到,才会执行。

一个 M 要执行 Go 代码,流程

优先本地P中获取G

1. M 绑定一个 P
2. M 从 P 的本地队列取一个 G(本地队列 无锁)
3. 执行这个 G
4. G 阻塞/让出/结束后,M 再从 P 找下一个 G

work-stealing机制:M获取G(如果 P 本地队列没有 G,才会去其他地方偷G

1.全局队列拿一批 G

2. 找网络轮询器里的就绪 G
3.从其他 P偷一半 G

hand-off机制:

场景:M绑定了P中的G1,执行G1,如果G1执行了阻塞动作。此时,执行“切换机制”

目的:

  1. 因为M绑定的P上还有其他的G,要将这个P让给其他的M继续执行
  2. M绑定的G1阻塞了,就只让M绑定G1,只让M等待这个G1

流程(分情况)

G 阻塞在 channel/锁时

  1. M继续持有P,继续执行P中其他的G
  2. 被阻塞的G从P中移除,挂到阻塞对象的等待队列

G 阻塞在 系统调用 时

  1. M先将绑定的P解绑,这个P会过度给其他的M继续执行
  2. M绑定阻塞的G1,会一直等待G1执行完被唤醒




视频链接

1.为什么引入协程?

一句话:进程线程太重了,需要用户态和内核态来回切换,协程是用户态切换,更加轻量

  1. 线程进程模型的弊端

    1. 为了解决多线程多进程频繁切换,导致的CPU浪费

    2. 多线程随着同步竞争(锁、竞争资源冲突),导致性能下降

    3. 占用内存:进程4GB、线程4MB

  2. 协程的优点

    1. 协程是用户态实现的,不需要经过内核态和用户态之间的切换,更加轻量

    2. 一个goroutine:几KB

    3. 灵活调度,切换成本低

2.早期的Go调度器

  1. 全局go协程队列,存放着M个协程g

  2. 有N个线程去全局go协程队列获取G执行,每次获取都需要加全局锁(锁竞争)

3.GMP模型简介

① M每次都先去获取P ② P再去获取G

一个线程M想执行协程G:M就要先去「空闲P队列」获取P,然后P和M绑定,之后P再依次去「本地协程队列、全局协程队列」获取G,将G交给线程M去执行

  1. G:协程

  2. M:thread线程(内核线程)

    1. 有一个M阻塞,会先从空闲M队列获取新的M,若没有,再去创建一个新的M

    2. 如果有M空闲,那么就会回收or放回空闲M队列

  3. P:processor处理器(每个P具有自己的协程本地队列,P管理了协程队列中的G)

    1. P的本地队列存放等待运行的G

    2. 优先将新创建的G存放在P的本地队列,本地队列满了,才会放到全局队列

  4. 除了P的本地队列,还有一个全局队列

数量

  1. M的数量:GO语言本身限定M的最大量是1w,一般设置为核心数(runtime/debug包中的SetMaxThreads函数来设置)

  2. P的数量问题

    1. 环境变量$GOMAXPROC,一般设置为 = 内核线程数/2

    2. 在程序中通过runtime.GOMAXPROCS()来设置

  3. G的数量问题

4.调度器的设计策略

复用线程、利用并行、抢占、全局G队列

4.1. work-stealing机制

概述

  • 场景:当本线程⽆可运⾏的G时,尝试「从其他线程绑定的P偷取G」
  • 获取的流程:
    • 本地队列获取任务
    • 全局队列获取任务
    • 其它M的本地队列窃取任务

case1:从全局队列中steal协程G

此时,M2内核线程绑定的P,没有协程G了,但是全局队列中有空闲的G

M2去全局队列中steal协程G3,存放在自己的P中

case2:从其他P中steal协程G

1. M1和P绑定,G1正在运行,M2线程是空闲的

2. 此时M2想执行,那么将会从M1绑定的P的本地队列中steal协程

4.2. hand-off机制(切换机制)

概述

  • 场景:当本线程因为G进⾏系统调⽤阻塞时,线程M释放绑定的P,把P转移给其他空闲的线程执⾏
  • 流程:当G阻塞时,与该G绑定的M也会陷入阻塞,在阻塞之前,会先把M绑定的P转移给其他M',然后将CPU切换到M'去执行

1. 假设,此时M1绑定的P队列中正在执行的协程G1,G1执行了一个阻塞操作,比如read。

2. hand-off执行过程

2.1. 首先,创建一个线程or唤醒一个睡眠状态的线程,如M3

2.2. 将M1绑定的P,迁移到M3上

2.3. 将G1与M1进行绑定,此时

① M1阻塞等待read事件的返回

② 内核线程切换到M3,通过P去获取本地队列中的G2,继续执行

这样就完成了hand-off机制

5. Go指令的调度流程

1~2步骤:执行go func(),先创建一个G,优先放入P的本地队列,如果满了,放入全局队列(此时P已经存放到P的本地队列)

3步骤:此时M获取G:优先从M的本地队列P中获取G,如果为空,依次去全局队列其他M的本地队列P去偷取G。(当获取P成功后,将P与M进行绑定)

4~6步骤:

之后,M1调度协程G,执行G的func()函数(备注,每个G的运行时间不超过10ms,防止其他G被饿死)

此时,G执行,执行分为以下情况

case1:G的执行时间片超时,即执行时间大于10ms,G会重新放到M1绑定的本地队列P中

case2:func函数执行了systemcall\阻塞(如read、write),则会获取新的M(从休眠M空闲队列or创建一个M)

若此时,M1的P队列还有很多G等待执行,因为M在执行G1时调用了systemcall\阻塞操作,所以,M1的P队列将交给新的M接管(hand-off机制)

执行完后的效果:①M1和G1捆绑 ②M3接管了M1的P

之后,与M1绑定的G1,因为处于阻塞状态,所以下一步会解除绑定关系,此时①M1销毁或者存放回休眠队列M中 ②G1放回全局队列中

6. 调度器的生命周期

M0

1. 启动程序后编号为0的主线程

2. 在全局变量runtime.m0中,不需要在heap上分配

3. 负责执行初始化和启动第一个G

4. 执行第一个G之后,M0就和其他的M一样了

G0

1. 每次启动一个M,都会第一个创建的G

2. G0仅用于负责调度G

3. G0指向任何可执行函数

4. 每一个M都会有一个自己的G0

5. 在调度或系统调用时就会使用M会切换到G0,来调度

6. M0的G0会放在全局空间

7. 场景分析

7.1.(场景1)G1创建G3

此时,存在M1、M2,每个M绑定了P,P上分别有一个G

此时,G1创建了G3:满足局部性,即G1创建的G3,应该存放在M1和G1所在的P上(如下图所示)

7.2.(场景2)G1执行完毕

当M1绑定的G1执行goexit(),G1执行完毕:M1继续获取G,优先从本地的P获取G

7.3.(场景3-4-5)

场景3:G2开辟过多的G

场景4:G2本地满,再创建G7

1. 将本地队列P拆分成2段

2. 将前一段和G7打散,再存放在本地队列中

场景5:G2本地未满,创建G8:直接将G8放到本地队列中

7.4.(场景6)唤醒正在休眠的M

M1与P1绑定,P1获取了G,此时,G2创建了G8

G2创建一个协程G8的时候

1. 首先尝试去休眠线程队列中,唤醒一个休眠的线程

2. 唤醒之后,将M从休眠线程队列中取出来

3. 此时,被唤醒的M2,将尝试与新的P绑定

一旦M2绑定了空闲的P,此时会调用G0

自旋线程M2的本地队列P2中没有G && M2正在运行G0去寻找G

7.5.(场景7)被唤醒的M2从全局队列中获取批量G

获取G的个数 N = min{ len(GQ)/GOMAXPROCS+1, len(GQ/2) } , GQ:全局队列的总长度

7.6.(场景8)M2从M1中批量偷取G

假设此时全局队列中没有G,M2就需要从其他M的P中获取G(批量个数N=后半段)

7.7.(场景9)自旋线程的最大限制

自旋线程 + 执行线程 <= GOMAXPROCS

此时,假设新创建了M5,因为GOMAXPROCS=4,不能在创建自旋线程了,所以,M5会被放入休眠线程队列1

7.8.(场景10)G发生系统调用/阻塞

1. M2的P2执行G8,此时G8执行了systemcall 阻塞(此时M2绑定了G8)

2. 因为此时M2的P2中存在G9,因为M2已经全权为G8负责了,为了不能阻塞G9的运行,所以P2会重新寻找有没有其他的M能继续为它执行(根据休眠线程队列中是否有空闲线程,分为两种情况)

2.1. 有M

P2将从空闲线程队列中取出M5,将P2挂到M5上(M5和P2组成新的MP)

2.2. 无M:将P放入空闲队列

7.9.(场景11)G发生系统阻塞,再变为非阻塞

M2中的G8,此时变为非阻塞,执行过程见下

1. M2中记录了上一次绑定的P,P是P2,即优先获取原配

2. M2发现P2已经被绑定给了M5,因此,M2是抢不过M5的

3. M2会先尝试从空闲P队列中寻找P

4.空闲P队列没有P,此时M2放弃绑定P,将执行释放逻辑:① M2放到空闲线程队列 ②G8放到全局P队列

8. Golang系统调用与阻塞处理 😄

8.1. 阻塞

8.1.1. Go阻塞的4种场景

  1. 由于原子、互斥量、通道操作调用导致 Goroutine 阻塞,调度器将把当前阻塞的 Goroutine 切换出去,重新调度 本地P队列 上的其他 Goroutine
  2. 由于网络请求、网络IO操作导致 Goroutine 阻塞。Go 程序提供了网络轮询器(NetPoller)来处理网络请求和 IO 操作的问题,其后台通过 kqueue(MacOS),epoll(Linux)或 iocp 来实现 IO 多路复用。通过使用 NetPoller 进行网络系统调用,调度器可以防止 Goroutine 在进行这些系统调用时阻塞 M。这可以让 M 执行 P 的 LRQ 中其他的 Goroutines,而不需要创建新的 M。执行网络系统调用不需要额外的 M,网络轮询器使用系统线程,它时刻处理一个有效的事件循环,有助于减少操作系统上的调度负载。用户层眼中看到的 Goroutine 中的“block socket”,实现了 goroutine-per-connection 简单的网络编程模式。实际上是通过 Go runtime 中的 netpoller 通过 Non-block socket + I/O 多路复用机制“模拟”出来的。
  3. 当调用一些系统方法的时候(如文件 I/O),如果系统方法调用的时候发生阻塞,这种情况下,网络轮询器(NetPoller)无法使用,而进行系统调用的 G1 将阻塞当前 M1。调度器引入 其它M 来服务 M1 的P。
  4. 如果在 Goroutine 去执行一个sleep操作,导致 M 被阻塞了。Go 程序后台有一个监控线程 sysmon,它监控那些长时间运行的 G 任务然后设置可以强占的标识符,别的 Goroutine 就可以抢先进来执行。

8.2. 系统调用与调度机制

8.2.1.异步系统调用

异步系统调用:网络IO

结论:当G1执行异步系统调用时,会发生阻塞,该阻塞动作,①不需要创建新的M,② G会和MP分离(G挂到netpoller),阻塞事件会由NetPoller接管

刚开始,G1在M上运行,此时G1想去执行「网络系统调用」

G1执行「网络系统调用」后,发生阻塞,此时,将G1挂在到NetPoller上&&监听G1网络系统调用的返回,M会从P队列中找到新的协程运行。(注:不需要创建新的M)

当G1的「网络系统调用」返回后,G1会被移回到P队列中

8.2.2.同步系统调用

同步系统调用:读写文件

结论:当G1执行同步系统调用时,G2会发生阻塞,同时会导致与G1绑定的M1也阻塞,之后,MG 会和P分离(P另寻M),当M从系统调用返回时,不会继续执行,而是将G放到run queue

刚开始,G1在M上运行,此时G1想去执行「同步系统调用」,G1会阻塞

同步调用,当G1阻塞后,会导致M1也阻塞,具体的执行动作是:G1和M1绑定在一起&&陷入阻塞,M1绑定的P会转移给新的M

阻塞的系统调用完成后:G1可以移回 LRQ 并再次由P执行。如果这种情况需要再次发生,M1将被放在旁边以备将来使用

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/11 9:22:34

期货回测成交太理想:天勤 TqSim commission 与实盘校准

前言 国内期货量化策略上线通常走三步&#xff1a;先用天勤 TqBacktest 在历史 K 线上回测双均线等信号&#xff0c;净值曲线往往很好看&#xff1b;再用 TqSim 或 TqKq 接实时行情做模拟盘&#xff1b;最后才挂 TqAccount 实盘。很多人卡在第二步&#xff1a;回测净值翻倍&…

作者头像 李华
网站建设 2026/6/11 9:22:29

别再手动算距离了!用Python的NumPy和Matplotlib搞定点到几何图形的最短距离(附完整代码)

几何距离计算实战&#xff1a;用Python向量化技术提升空间分析效率在游戏碰撞检测、地理围栏分析或工业机器人路径规划中&#xff0c;计算点到几何对象的距离是高频操作。传统循环遍历方法在面对海量数据时性能堪忧&#xff0c;而基于NumPy的向量化运算可轻松实现百倍加速。本文…

作者头像 李华
网站建设 2026/6/11 9:22:23

Gooey:Zig 语言打造跨平台 UI 框架,挑战 Electron 与 Qt 统治地位

【导语&#xff1a;在系统编程领域&#xff0c;Zig 语言社区长期缺乏生产级 GUI 方案。近期 GitHub 上出现的实验性项目 Gooey&#xff0c;试图改变这一现状&#xff0c;它是完全用 Zig 编写的 GPU 加速跨平台 UI 框架&#xff0c;虽有局限&#xff0c;但前景值得期待。】填补 …

作者头像 李华
网站建设 2026/6/11 9:22:22

用Python手把手教你实现QAM/PSK星座图的格雷映射(附完整代码)

Python实战&#xff1a;从零构建QAM/PSK星座图的格雷映射系统通信工程师工具箱里最迷人的工具之一&#xff0c;莫过于星座图——那些漂浮在复平面上的神秘点阵。但你是否想过&#xff0c;为什么相邻星座点之间通常只有一位二进制差异&#xff1f;这背后藏着格雷码的智慧。今天&…

作者头像 李华
网站建设 2026/6/11 9:22:17

分场景板材边缘间隙标准大全,匹配DFA设计落地执行规范

板材边缘间隙没有通用的 “万能数值”&#xff0c;不同 PCB 工艺、封装类型、拼板方式、装配结构、应用场景&#xff0c;对应的间隙标准差异极大。很多工程师设计时仅凭经验取值&#xff0c;要么间隙过大造成空间浪费&#xff0c;产品体积超标&#xff1b;要么间隙过小触发工艺…

作者头像 李华
网站建设 2026/6/11 9:22:13

钉钉消息防撤回补丁PC版:保护企业沟通记录的终极解决方案

钉钉消息防撤回补丁PC版&#xff1a;保护企业沟通记录的终极解决方案 【免费下载链接】DingTalkRevokeMsgPatcher 钉钉消息防撤回补丁PC版&#xff08;原名&#xff1a;钉钉电脑版防撤回插件&#xff0c;也叫&#xff1a;钉钉防撤回补丁、钉钉消息防撤回补丁&#xff09;由“吾…

作者头像 李华