GMP 调度模型
C++ 里你要手写线程池、任务队列、负载均衡;Go 用 G/M/P 三件套在运行时层面把它全包了 一、简述 goroutine 不是 OS 线程,它比线程轻几个数量级。让几十万 goroutine 复用少数几个内核线程的,就是本章主角——GMP 调度模型。学完你会明白:go func() 背后发生了什么、为什么 goroutine 能在系统调用阻塞时"让位"、GOMAXPROCS 到底是什么、以及 M:N 用户态协程调度器是如何设计的。 核心要点:G(goroutine,任务)、M(Machine,OS 线程,工人)、P(Processor,调度上下文,工位)。P 的数量才是真正同时跑的 “线程数”,G 挂在 P 的本地队列上排队执行,M 抢到 P 才干活。运行时自动完成了 C++ 里你手写线程池 + 任务队列 + work-stealing 的全部工作。 二、C++ 对照速查表 概念 C++ Go 关键差异 并发执行单元 std::thread goroutine (G) std::thread 直接映射 OS 线程,G 是用户态任务 线程池 自写 / BS::thread_pool 等 内置 GMP Go 运行时自动复用 M,无需自建池 任务队列 std::queue<std::function> + mutex P 本地队列 runq 每个 P 有 256 长的本地队列,另有全局限队列 线程数量 自己定 / std::thread::hardware_concurrency GOMAXPROCS(默认 = CPU 核数) P 的数量决定并行度,M 可多于 P 线程阻塞迁移 无自动机制 M 阻塞时 P 移交 系统调用/锁阻塞时,P 被交给其它 M 继续执行 G 栈 固定大小(默认 1~8MB) 初始 2KB,动态增长 G 栈可扩容,按需分配 让出 CPU 无(靠 OS 时间片) runtime.Gosched() G 主动让出 P,避免饿死其它 G 抢占 内核时间片抢占 运行时信号抢占(Go 1.14+) 协作式 + 抢占式混合 work-stealing 手写 内置 空闲 P 偷其它 P 的队列任务,均衡负载 主线程 int main() m0 + g0 运行时专用线程和调度 goroutine 三、逐主题详解 3.1 三个字母分别是什么 ┌──────────────────────────────────────────────────────────────┐ │ GMP 关系示意 │ │ │ │ G (goroutine) M (machine) P (processor) │ │ ┌──────┐ ┌───────┐ ┌─────────────┐ │ │ │ 用户 │ │ 内核 │ │ 调度上下文 │ │ │ │ 代码 │──调度于──→ │ OS线程 │──绑定于──→│ 本地队列 │ │ │ │ 栈G │ │ 栈M │ │ runq[256] │ │ │ └──────┘ └───────┘ └─────────────┘ │ │ 任务 工人 工位 │ └──────────────────────────────────────────────────────────────┘ G(goroutine):一个待执行的函数 + 它的栈 + 上下文(寄存器现场)。创建的每个 go func() 都对应一个 G。它只是任务元数据,不是执行载体。G 结构体字段很长(src/runtime/runtime2.go),核心有栈描述 stack、当前函数 sched(保存 PC/SP)、状态 atomicstatus、所属 P 的指针、是否可被抢占等。 M(Machine):真实的 OS 线程,由内核调度。M 负责真正执行 G 的机器码。创建 M 要付出系统调用成本(约 1MB 内核栈 + 内核结构,线程栈),远贵于创建 G。M 通常不超过几千个,且有上限(受系统资源约束,默认 maxmcount=10000)。 P(Processor):你可以把它理解成"工位/许可令牌"。P 里有本地可运行队列 runq(长度 256)。G 想被调度运行,必须先被放进某个 P 的 runq,然后 P 绑定到某个 M 上执行队列里的 G。 P 的数量 = GOMAXPROCS(默认取 CPU 逻辑核数),它决定了同一时刻真正并行执行的 goroutine 上限。P 多了,并行度就高;P = 1 时行为接近单线程。 C++ 对照:M ≈ 一个真正运行在 CPU 上的线程;P ≈ 每个线程手头那个任务队列;G ≈ 任务队列里的任务项。你写线程池时是"一个线程,抢全局任务队列",Go 优化成"每线程一队列 + 偷取",减少锁竞争。 ...
豫公网安备41108202000982号