Files
cs-note/hzh/GolangStar/Go语言原理/gmp调度原理/gmp-overview.md
T

5.6 KiB
Raw Blame History

tags, create time
tags create time
go
golang
go-principle
gmp-scheduler
overview
2026-06-07 16:00

GMP 调度原理 — 概览

概述

本文档是 Go GMP 调度模型的入门篇章,从宏观架构角度回答三个核心问题:Go 为什么不用原生 OS 线程做并发?G/M/P 三个角色各自做什么?它们之间如何协作完成工作窃取、负载均衡和 IO 轮询?

读完本节后,你将建立起 GMP 的全局视图,后续章节将深入源码层面拆解每个组件的数据结构和调度流程。

正文

1.1 从线程到协程

在理解 goroutine 之前,需要先区分两个基础概念:线程(Thread) 和 协程(Coroutine)。

graph LR
    A["内核态"] --> B["线程<br/>OS 管理<br/>切换成本高"]
    C["用户态"] --> D["协程<br/>自我管理<br/>切换成本极低"]
    style A fill:#fce4ec
    style C fill:#e8f5e9
    style B fill:#fff9c4
    style D fill:#e3f2fd
  • 线程:操作系统内核管理的执行单元,每次上下文切换需要陷入内核态(保存寄存器、切换页表等),开销较大但稳定性高。
  • 协程:运行在用户态的轻量级执行流,由应用程序自主调度,切换仅需修改栈指针,开销极小。

结论:线程重而稳,协程轻而快。

1.2 Go 的答案:goroutine

Go 没有直接采用传统协程模型,而是在其之上构建了 GMP 调度体系,使 goroutine 获得两大核心优势:

  1. 灵活调度:G(goroutine)、M(machine,OS 线程)、P(processor,逻辑处理器)三者可动态绑定和解绑,支持水平伸缩。
  2. 动态栈:初始栈仅 2~8KB,随使用量自动扩容或收缩,兼顾便利性和内存利用率。

Go 在顶层屏蔽了系统线程的概念——开发者只与 goroutine 打交道,调度细节全部由 runtime 接管。

1.3 GMP 架构全景

角色 职责 类比(工厂)
G 任务单元,包含执行栈、状态、要执行的函数体 工作任务
M OS 线程封装,真正执行代码的"工人" 工人
P 调度器,管理本地队列,协调 M 与 G 的关系 车间主管
graph TB
    subgraph P0["P0"]
        LRQ0["LRQ: G5 → G6 → G7"]
    end
    subgraph P1["P1"]
        LRQ1["LRQ: G8"]
    end
    GRQ["GRQ 全局队列<br/>[G1, G2, G3, G4]"]
    M0["M0"] --> P0_node["P0"]
    M1["M1"] --> P1_node["P1"]
    P0_node --> LRQ0
    P1_node --> LRQ1
    LRQ0 -.work-stealing.-> LRQ1
    LRQ1 -.work-stealing.-> LRQ0
    GRQ -->|"LRQ 空时取"| P0_node
    GRQ -->|"LRQ 空时取"| P1_node
    style GRQ fill:#fff9c4
    style LRQ0 fill:#e8f5e9
    style LRQ1 fill:#e8f5e9

两种队列

G 存放在两种队列中,获取优先级由高到低:

队列 说明 锁 容量
LRQ(Local Run Queue) 每个 P 私有的 G 队列 无锁 CAS 最多 256 个
GRQ(Global Run Queue) 所有 P 共享的全局队列 sched.lock 理论上无限

放置策略:新创建的 G 优先放入当前 P 的 LRQ;LRQ 满时才加锁放入 GRQ。

获取策略(findrunnable 的执行顺序):

flowchart TD
    A["LRQ<br/>无锁 CAS,最快"] -->|"空"| B{"GRQ<br/>需加锁"}
    B -->|"空"| C["netpoll IO 就绪<br/>非阻塞 epoll_wait"]
    B -->|"有"| E["找到 G ✓"]
    C -->|"有"| E
    C -->|"空"| D["work-stealing<br/>偷其他 P 的一半"]
    D -->|"成功"| E
    D -->|"失败"| F["P/M 进入休眠"]
    style A fill:#e8f5e9
    style B fill:#fff3e0
    style C fill:#fff9c4
    style D fill:#ffebee
    style F fill:#eceff1

[!note] 📝 防饥饿机制 为防止 GRQ 中的 G 长期饿死,每 61 次调度循环强制检查一次 GRQ,保证公平性。

1.4 GMP 生态圈

GMP 是 Go 运行时的心跳中枢,上层多数子系统都围绕它设计。

1.4.1 内存管理

Go 借鉴 Google TCMalloc 思想,为每个 P 配备私有缓存 mcache。P 上的 G 分配小对象时直接从 mcache 取,完全无锁。

graph LR
    G1["G₁"] -->|无锁| MC1["P₀ → mcache"]
    G2["G₂"] -->|无锁| MC1
    MC1 -->|"mcache 耗尽"| MH["mcentral → mspan"]
    MH -->|"mspan 耗尽"| MM["mmapped 直接映射"]
    style MC1 fill:#e8f5e9
    style MH fill:#fff9c4
    style MM fill:#fce4ec

1.4.2 并发工具(Mutex / Channel)

Go 的同步原语是 G 级别 的——当 G 因等待 channel 数据或 Mutex 而阻塞时,它调用 gopark 主动让出 M,而不是阻塞整条 OS 线程。这使得单个 OS 线程上可以承载海量并发。

[!question] ❓ 对比思考 C++ 标准库锁一旦持有,阻塞的是整个线程,该线程上的所有协程都被迫等待。Go 通过 G 级别阻塞实现了更细粒度的并发控制。

1.4.3 IO 多路复用(netpoll)

网络 IO 场景下,Go 使用 Linux epoll 作为底层轮询机制,并通过 netpoll 将其包装为 G 级别的异步通知:

  • G 发起 IO → gopark 挂起自身
  • OS 就绪 → netpoll 发现 → goready 唤醒 G

IO 操作由此无缝融入 GMP 调度环,无需额外线程。

关联笔记