文章

详解 GO GPM并发模型

Go 语言的并发模型是其核心特性之一,而 GPM 模型 是 Go 实现高效并发的基石。GPM 分别代表:

  • G:Goroutine(协程)

  • P:Processor(处理器)

  • M:Machine(线程)

下面我们将详细解析 GPM 模型,并通过图解帮助理解。


1. GPM 模型的核心组件

1.1 Goroutine(G)

  • Goroutine 是 Go 语言中的轻量级线程,由 Go 运行时管理。

  • 每个 Goroutine 初始只有几 KB 的栈空间,且栈空间可以动态扩容。

  • Goroutine 的创建和切换成本远低于操作系统线程。

1.2 Processor(P)

  • P 是 Go 运行时管理的逻辑处理器,负责调度 Goroutine。

  • P 的数量默认等于 CPU 核心数(可通过 GOMAXPROCS 调整)。

  • 每个 P 维护一个本地 Goroutine 队列(Local Run Queue, LRQ)。

1.3 Machine(M)

  • M 是操作系统线程,由操作系统调度。

  • M 负责执行 Goroutine 的代码。

  • 每个 M 必须绑定一个 P 才能执行 Goroutine。


2. GPM 模型的工作机制

2.1 基本关系

  • P 和 M 的关系

    • 每个 M 必须绑定一个 P 才能执行 Goroutine。

    • 一个 P 可以绑定多个 M,但同一时间只能有一个 M 在执行。

  • P 和 G 的关系

    • 每个 P 维护一个本地 Goroutine 队列(LRQ)。

    • 当 LRQ 为空时,P 会从全局 Goroutine 队列(Global Run Queue, GRQ)或其他 P 的 LRQ 中偷取 Goroutine。

2.2 调度流程

  1. Goroutine 创建

    • 当一个 Goroutine 被创建时,它会被放入当前 P 的 LRQ 中。

    • 如果 LRQ 已满,则放入全局 Goroutine 队列(GRQ)。

  2. M 绑定 P

    • M 需要绑定一个 P 才能执行 Goroutine。

    • 如果当前没有空闲的 P,M 会阻塞,直到有 P 可用。

  3. Goroutine 执行

    • M 从绑定的 P 的 LRQ 中获取 Goroutine 并执行。

    • 如果 LRQ 为空,M 会尝试从 GRQ 或其他 P 的 LRQ 中偷取 Goroutine。

  4. 系统调用

    • 当 Goroutine 执行系统调用时,M 会解绑 P,并将 P 交给其他 M 使用。

    • 系统调用完成后,M 会尝试重新绑定 P,并继续执行 Goroutine。

  5. Goroutine 阻塞

    • 当 Goroutine 阻塞(如等待 Channel)时,M 会解绑 P,并将 P 交给其他 M 使用。

    • 当 Goroutine 解除阻塞时,它会被重新放入某个 P 的 LRQ 中。


3. GPM 模型的图解

以下是 GPM 模型的示意图:

+-------------------+       +-------------------+       +-------------------+
|       M1          |       |       M2          |       |       M3          |
|  +-------------+  |       |  +-------------+  |       |  +-------------+  |
|  |     P1      |  |       |  |     P2      |  |       |  |     P3      |  |
|  |  +-------+  |  |       |  |  +-------+  |  |       |  |  +-------+  |  |
|  |  |  G1  |  |  |       |  |  |  G2  |  |  |       |  |  |  G3  |  |  |
|  |  +-------+  |  |       |  |  +-------+  |  |       |  |  +-------+  |  |
|  +-------------+  |       |  +-------------+  |       |  +-------------+  |
+-------------------+       +-------------------+       +-------------------+
        |                           |                           |
        |                           |                           |
        v                           v                           v
+-------------------+       +-------------------+       +-------------------+
|  Global Run Queue |       |  Global Run Queue |       |  Global Run Queue |
|  +-------+        |       |  +-------+        |       |  +-------+        |
|  |  G4  |        |       |  |  G5  |        |       |  |  G6  |        |
|  +-------+        |       |  +-------+        |       |  +-------+        |
+-------------------+       +-------------------+       +-------------------+
  • M1, M2, M3:操作系统线程。

  • P1, P2, P3:逻辑处理器,每个 P 绑定一个 M。

  • G1, G2, G3:正在执行的 Goroutine。

  • G4, G5, G6:全局 Goroutine 队列中的 Goroutine。


4. GPM 模型的优势

4.1 高效的 Goroutine 调度

  • 本地队列(LRQ):减少锁竞争,提高调度效率。

  • 工作偷取(Work Stealing):当某个 P 的 LRQ 为空时,可以从其他 P 的 LRQ 或 GRQ 中偷取 Goroutine,确保负载均衡。

4.2 动态扩展

  • Goroutine 的栈空间可以动态扩展,初始只有几 KB,避免内存浪费。

  • M 的数量可以动态调整,适应不同的并发需求。

4.3 减少线程切换

  • Goroutine 的切换由 Go 运行时管理,成本远低于操作系统线程切换。


5. GPM 模型的局限性

5.1 系统调用阻塞

  • 当 Goroutine 执行系统调用时,M 会解绑 P,可能导致 P 的利用率下降。

5.2 全局队列竞争

  • 当多个 P 同时访问全局队列时,可能会产生锁竞争。

5.3 调试复杂性

  • GPM 模型的动态调度机制增加了调试和性能分析的难度。


6. 实际应用中的优化建议

6.1 控制 Goroutine 数量

  • 避免创建过多的 Goroutine,尤其是在高并发场景中。

6.2 减少系统调用

  • 尽量使用非阻塞 I/O 或异步操作,减少系统调用的阻塞。

6.3 使用 sync.Pool

  • 复用对象,减少堆分配和 GC 压力。

6.4 调整 GOMAXPROCS

  • 根据实际 CPU 核心数调整 GOMAXPROCS,以充分利用多核性能。


总结

GPM 模型是 Go 语言高效并发的核心机制,通过 Goroutine、Processor 和 Machine 的协同工作,实现了轻量级、高并发的任务调度。理解 GPM 模型的工作原理,有助于编写高效的并发程序,并优化性能瓶颈。

License:  CC BY 4.0