go从零单排之GPM

0 阅读4分钟

Go GMP 调度器


一、GMP 到底是什么?

  • G (Goroutine) :用户轻量级线程,包含栈、指令、状态,很小(2KB 起)
  • P (Logical Processor)逻辑处理器,承载 G,连接 M 与 G,必须有 P 才能运行 G
  • M (Machine)系统线程(OS Thread) ,真正被 CPU 执行的载体,绑定 P 才能跑 G

口诀

M 必须绑定 P 才能运行 G

P 持有 G 队列

M 从 P 拿 G 执行

P 数量 = GOMAXPROCS(真正并行度)


二、GMP 核心结构体

文件:runtime/runtime2.go

1. G (Goroutine)

type g struct {
        stack       stack   //  goroutine 栈:栈顶、栈底
        sched       gobuf   // 调度上下文:保存寄存器(PC、SP、BP等)
        goid        int64   // 协程ID
        status      uint32  // 状态:Gidle/Grunnable/Grunning/Gsyscall/Gwaiting...
        waitreason  string  // 等待原因
        m           *m      // 当前绑定的M(正在哪个M上跑)
        p           *p      // 当前绑定的P
}
// 调度上下文(切换时保存CPU现场)
type gobuf struct {
        sp   uintptr // 栈指针
        pc   uintptr // 程序计数器
        ret  uintptr // 返回值
        ...
}

2. P (Logical Processor)

type p struct {
        m           *m      // 当前绑定的M
        status      uint32  // 状态:Pidle/Prunning/Psyscall/Pgcstop
        runq        [256]guintptr // 本地G队列(无锁,极快)
        runqhead    uint32  // 队列头
        runqtail    uint32  // 队列尾
        runqsize    int32   // 本地队列大小
        gfree       *g      // G缓存池(复用G,减少分配)
}

3. M (Machine/OS Thread)

type m struct {
        g0          *g      // 系统栈G(调度、系统调用专用)
        curg        *g      // 当前正在运行的用户G
        p           *p      // 绑定的P
        nextp       *p      // 即将绑定的P
        mOS         mOS     // 系统线程相关信息
        lockedg     *g      // 被锁定的G(LockOSThread)
}

4. 全局调度器(sched)

var sched struct {
        mutex      mutex      // 全局锁
        globalRunq gQueue     // 全局G队列
        pidle      []*p       // 空闲P队列
        nmidle     int32      // 空闲M数量
        nmrunning  int32      // 运行中M数量
}

三、GMP 核心状态

G 状态

  • Gidle:空闲
  • Grunnable:可运行(等待 P 执行)
  • Grunning:正在运行
  • Gsyscall:系统调用中
  • Gwaiting:等待(锁 /chan)

P 状态

  • Pidle:空闲
  • Prunning:运行中
  • Psyscall:系统调用
  • Pgcstop:GC 停止

四、GMP 核心机制

1. 多级队列调度

  • P 本地 runq:无锁,最快
  • 全局 runq:有锁,备用
  • netpoller:异步 IO 就绪 G

2. work stealing(偷任务)

当 P 本地队列为空,去别的 P 偷一半 G来执行。

3. hand off(P 剥离)

G 进入系统调用 → M 与 P 剥离 → P 可以被其他 M 绑定继续执行 G。

4. 抢占调度(Go 1.14+ 异步抢占)

防止 G 长期占用 CPU,每 10ms触发一次抢占。


五、核心调度源码(逐行超详细注解)

文件:runtime/proc.go

1. 调度主循环:schedule () —— 灵魂函数

// schedule 调度主循环:M 不断寻找 G 执行
func schedule() {
        // 死循环:M永不退出
        for {
                // 1. 禁止GC抢占
                mp := acquirem()
                // 2. 尝试从 P 本地队列取 G(最快,无锁)
                if gp, inheritTime := runqget(mp.p); gp != nil {
                        // 本地有G → 直接执行
                        execute(gp, inheritTime)
                }
                // 3. 尝试从 全局队列 取 G
                if gp := globrunqget(mp.p, 1); gp != nil {
                        execute(gp, false)
                }
                // 4. 尝试从 netpoller 取IO就绪G
                if gp := netpoll(false); gp != nil {
                        execute(gp, false)
                }
                // 5. 偷任务:从其他P偷G(work-stealing)
                if gp := findrunnable(); gp != nil {
                        execute(gp, false)
                }
                // 6. 没找到G → M进入idle,释放P
                mput(mp)
        }
}

2. 执行 G:execute ()

// execute 真正开始运行G
func execute(gp *g, inheritTime bool) {
        // M绑定G
        mp.curg = gp
        // G状态改为运行中
        gp.status = Grunning
        // 恢复调度上下文(PC/SP)
        gogo(&gp.sched)
}

3. G 切换:gogo () —— 汇编实现(保存 / 恢复 CPU 现场)

TEXT runtime·gogo(SB), NOSPLIT, $0-8
    MOVQ    buf+0(FP), DX       // DX = &gp.sched
    MOVQ    gobuf_sp(DX), SP    // 恢复栈指针 SP
    MOVQ    gobuf_pc(DX), PC    // 恢复程序计数器 PC → 跳转到G执行

4. 主动让出:Gosched ()

func Gosched() {
        // 保存当前G上下文
        m.pcbuf = getcallerpc()
        // 让出P,进入Grunnable
        runtime.Gosched()
}

5. 系统调用剥离 P:entersyscall ()

func entersyscall() {
        // 系统调用开始 → M与P剥离
        releasem(mp)
        // P状态改为Psyscall
        mp.p.status = Psyscall
}

6. 异步抢占(10ms)

func preemptMS() {
        // 每10ms给运行中的G发送抢占信号
        // 让G在函数序言时主动让出CPU
}

六、GMP 全流程流程图

1. G 创建 → 调度 → 执行 完整流程

image.png

2. Work Stealing 偷任务流程

image.png

3. 系统调用 Hand Off 流程

image.png

4. 异步抢占调度流程

image.png


七、GMP 10 个细节

1. 真正并行度 = P 数量(GOMAXPROCS)

M 可以很多,但只有 P 能让 G 并行

2. P 本地队列无锁,全局队列有锁

所以优先跑本地队列。

3. G 不直接绑定 OS 线程,M 才是

G 是轻量级,M 是重量级。

4. M 可以比 P 多

当阻塞时,调度器会创建新 M 绑定 P。

5. Work Stealing 是 Go 高并发核心

让所有 CPU 满载。

6. 系统调用不会阻塞 P

P 会被剥离,继续服务其他 M。

7. Go 1.14+ 支持基于信号的异步抢占

解决死循环霸占 CPU 问题。

8. G 栈极小(2KB),可动态扩容

所以能百万并发。

9. g0 栈是系统栈

调度、GC 都在 g0 栈执行。

10. 全局队列只是兜底

绝大多数 G 永远走本地队列。


八、总结

  • G:任务
  • P:队列 + 资源
  • M:执行载体
  • M 必须绑定 P 才能跑 G
  • P 本地队列无锁最快
  • 偷任务保证 CPU 满载
  • 系统调用自动剥离 P
  • 抢占防止长期占用