基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

Go 语言的 GMP 调度模型

知识点图片

Go 语言之所以能够轻松支持百万级并发,其核心就在于它的调度器模型——GMP 模型

GMP 是 Go 运行时(Runtime)调度层面的三个核心组件的缩写:G (Goroutine)M (Machine)P (Processor)

下面我将从概念、模型演变、核心策略和工作流程四个方面深入浅出地为你讲解。


1. 核心组件介绍 (G、M、P)

你可以把 GMP 模型想象成一个建筑工地

  • G (Goroutine - 协程/任务):

    • 概念:Go 语言中的协程,用户级线程。
    • 特点:非常轻量(初始栈仅 2KB),由 Go 运行时管理,而非操作系统。
    • 比喻“砖头”或“任务单”。这是具体要干的活。
  • M (Machine - 内核线程):

    • 概念:操作系统内核线程(OS Thread)。
    • 特点:它是真正执行计算资源的载体。M 必须绑定 P 才能执行 G。
    • 比喻“建筑工人”。只有工人才能真正动手搬砖。
  • P (Processor - 处理器/上下文):

    • 概念:逻辑处理器,包含了运行 G 所需的资源(如本地队列、内存分配器等)。
    • 特点:P 的数量决定了系统最大并行度,由 GOMAXPROCS 决定。
    • 作用:连接 G 和 M 的桥梁。M 只有拿到 P,才能去执行 G。
    • 比喻“工卡”或“施工队”。工人(M)必须拿到工卡(P)才能进场干活,工卡里夹着一堆任务单(G)。

2. 为什么要引入 P?(GM 模型 vs GMP 模型)

在 Go 1.1 之前,使用的是 GM 模型,没有 P。

  • GM 的问题:所有的 M 都去同一个全局队列里抢 G 执行。为了安全,必须加一把全局大锁
  • 后果:锁竞争极其激烈,性能很差;M 的缓存局部性差。

引入 P 之后
P 拥有一个本地队列 (Local Run Queue)

  • M 拿到 P 后,优先从 P 的本地队列取 G 执行,不需要加锁(因为是本地的)。
  • 只有本地没货了,才去全局队列或偷别人的,大大减少了锁竞争。

3. GMP 的核心调度策略

Go 调度器为了让“工人”不闲着,且活干得快,设计了以下几个关键机制:

A. 复用线程 (Work Stealing & Hand Off)

避免频繁创建和销毁操作系统线程(M),而是复用它们。

  1. Work Stealing (工作窃取机制)

    • 当一个 M 绑定的 P 的本地队列空了,M 不会休息。
    • 它会先去全局队列看有没有 G。
    • 如果全局也没有,它会去其他 P 的本地队列“偷”一半的 G 过来执行。
    • 目的:负载均衡,不让任何一个线程闲置。
  2. Hand Off (握手/分离机制)

    • 当 M 执行的 G 进行系统调用阻塞(如读写文件)时,M 会被阻塞住。
    • 为了不让 P 闲着(P 里还有其他 G 等着跑),P 会与当前的 M 分离(Detach)。
    • P 会去寻找一个新的 M(或新建一个 M)来继续执行 P 队列里剩下的 G。
    • 当旧的 M 阻塞结束,它会尝试获取一个空闲的 P;如果拿不到,就把它的 G 放入全局队列,自己休眠。
    • 目的:提高资源利用率,不让阻塞阻碍整体进度。

B. 抢占式调度 (Preemption)

  • 问题:如果一个 G 写了个死循环 for {},一直占用 M 和 P 怎么办?
  • 解决:Go 有一个后台监控线程 sysmon。它会监控运行时间过长的 G(超过 10ms)。
  • 动作:sysmon 会强行把这个 G 也就是“踢”出去,放到全局队列尾部,让其他 G 有机会执行。
  • 注:Go 1.14 引入了基于信号的异步抢占,解决了死循环无法被抢占的问题。

C. 网络轮询器 (Netpoller)

  • 当 G 进行网络 I/O(如 HTTP 请求)时,不会阻塞 M
  • G 会被扔给 Netpoller 等待网络数据,M 继续执行其他 G。
  • 当网络数据到了,Netpoller 会把 G 放回 P 的队列中。
  • 意义:这是 Go 写网络高并发服务极其高效的原因。

4. 完整的调度流程图解

假设一个 G (G1) 被创建:

  1. 放入队列

    • G1 优先放入当前 P 的本地队列
    • 如果本地队列满了(256个),G1 会被放入全局队列(需要加锁)。
  2. M 获取 G

    • M 绑定 P,从 P 的本地队列弹出一个 G 开始执行。
  3. 执行过程

    • 正常结束:G 执行完,M 销毁 G,继续取下一个。
    • 发生阻塞 (Syscall):M 和 P 分离,P 找新 M。
    • 发生网络 I/O:G 移交 Netpoller,M 继续干活。
    • 本地空闲:M 发现本地没 G 了,去全局队列取;全局也没了,去偷别的 P 的 G。

5. 总结

GMP 模型的高效在于:

  1. M:N 模型:N 个协程(G)复用 M 个内核线程,既节省了线程创建开销,又利用了多核优势。
  2. P 的本地队列:消除了全局锁的性能瓶颈(无锁访问)。
  3. Work Stealing:实现了自动负载均衡。
  4. Netpoller:将网络阻塞转化为非阻塞,完美契合高并发 I/O 场景。

这就是 Go 语言“高并发、高性能”背后的秘密武器。

00:00
00:00