go运行时调度器采用gmp模型:g是轻量级协程,m是os线程代理,p是调度逻辑单元;三者协同实现用户态高效并发调度,不依赖操作系统线程调度。

Go 的运行时调度器采用 GMP 模型,是 Go 并发高效的核心——它不依赖操作系统线程调度,而是由 Go 运行时在用户态自主管理协程(Goroutine)的创建、切换与执行。
G 是什么:轻量级协程
G 代表 Goroutine,是 Go 程序中并发执行的最小单位。每个 G 持有独立的栈(初始仅 2KB,按需动态伸缩)、程序计数器和寄存器上下文。G 不绑定 OS 线程,也不直接参与系统调用;它的生命周期由运行时完全控制,创建开销极小(远低于线程),可轻松启动数十万实例。
- 新建 G 时,运行时从空闲 G 池(free list)复用或分配新结构体,初始化栈和状态(_Grunnable)
- G 执行函数入口后进入 _Grunning 状态;阻塞(如 channel 等待、系统调用)时转为 _Gwaiting 或 _Gsyscall
- 当 G 完成或 panic,运行时将其清理并放回空闲池,供后续复用
M 是什么:OS 线程的代理
M(Machine)是对底层操作系统线程(如 pthread)的封装,负责实际执行 G 的指令。每个 M 持有一个内核线程,能绑定一个 P(Processor),并维护自己的栈、寄存器及信号掩码。M 的数量默认无硬性上限,但受 runtime.GOMAXPROCS 控制活跃 M 数(避免过多线程竞争)。
- M 启动时尝试获取一个空闲 P;若失败,则休眠等待(park)直到被唤醒或分配到 P
- M 在执行 G 期间若遇到阻塞式系统调用(如 read/write),会将 P 转交其他 M,自身脱离 P 继续完成系统调用(此时状态为 _Msyscall)
- 系统调用返回后,M 需重新“抢”一个 P(可能原 P 已被占用),才能继续执行 G;抢不到则进入休眠队列
P 是什么:调度资源的逻辑单元
P(Processor)是调度器的关键枢纽,既非线程也非协程,而是一个逻辑处理器,用于管理 G 的本地队列、内存分配缓存(mcache)、定时器、netpoll 等资源。P 的数量由 GOMAXPROCS 决定(默认等于 CPU 核心数),且必须 ≥1。所有活跃 M 必须绑定一个 P 才能运行 G。
立即学习“go语言免费学习笔记(深入)”;
- 每个 P 维护一个本地 G 队列(长度上限 256),新创建的 G 优先加入当前 P 的本地队列
- 当本地队列为空,M 会尝试从全局 G 队列(sched.runq)偷取一批 G;若仍为空,则进行 work-stealing —— 随机选取其他 P 的本地队列窃取一半 G
- P 还承载 netpoller(基于 epoll/kqueue/IOCP),异步监听网络 I/O 就绪事件,并将关联的 G 唤醒至本地队列
一次典型 G 执行流程
以 go f() 启动一个新协程为例,其背后经历多个运行时介入环节:
- 编译器将 go f() 编译为对 runtime.newproc 的调用,传入函数指针和参数地址
- newproc 分配 G 结构体,设置栈、状态、入口函数,然后将其加入当前 P 的本地队列(若本地队列未满)或全局队列(若满)
- 当前 M 执行完当前 G 后,调用 schedule() 函数:先检查本地队列,再查全局队列,最后尝试窃取;找到 G 后切换至 _Grunning 状态
- M 使用 setcontext 或汇编指令(如 x86-64 的 CALL/RET 模拟)切换到 G 的栈和指令位置,开始执行 f()
- f() 中若发生 channel 操作、time.Sleep、网络读写等,会触发 runtime.gopark,将 G 置为等待态,并让出 P 给其他 G;待条件满足(如 channel 有数据、定时器到期、socket 可读),runtime.ready 将其重新加入某个 P 的运行队列










