Go 的 GMP 模型
Go 语言的并发模型使用的是一种称为 GMP 模型(Goroutine, Scheduler, and P)的机制,它通过goroutine(协程)、调度器(Scheduler)以及**处理器(P)**的组合来实现高效的并发控制。
GMP 模型是 Go 语言并发模型的核心,它通过将并发任务拆分到多个处理器上,使得 Go 语言的并发可以高效地运行,并且能够充分利用多核 CPU。
GMP 模型组成部分
- G (Goroutine):
- Goroutine 是 Go 中的轻量级线程,它是 Go 程序并发的基本单位。
- 一个 Go 程序启动时通常会有一个初始的 Goroutine(通常是主 Goroutine),可以通过
go
关键字来创建新的 Goroutine。
- Goroutine 是用户级的线程,它比操作系统的线程更加轻量。通常,操作系统线程的创建和切换需要较多的系统资源,而 Goroutine 在 Go 语言中是通过 Go 的运行时(runtime)调度的,不依赖操作系统的线程。
- M (Machine):
- M 代表的是 操作系统线程,即 Go 调度器在底层调度的线程,负责执行实际的任务。
- 每个 M 代表着一个操作系统线程,它持有一个或者多个 Goroutine 的执行任务。
- M 的数量通常与机器的 CPU 核心数相关,多个 M 可能会在同一个处理器 P 上运行,P 会调度 M 来执行任务。
- P (Processor):
- P 是 处理器,代表的是一个逻辑处理器,用于调度和管理 Goroutine。
- 每个 P 可以运行多个 Goroutine,P 是 Go 运行时调度的核心,负责分配计算资源给 M 以执行 Goroutine。
- P 的数量决定了 Go 调度器的并发度(即最大能并行执行的 Goroutine 数量),通常设置为 CPU 核心数,但可以根据需要调整。
GMP 模型的工作原理
Go 语言的 GMP 模型通过以下几个步骤管理 Goroutine 的调度:
- 创建 Goroutine(G):
- 调度器调度:
- Go 运行时有一个全局调度器,负责调度 Goroutine 的执行。调度器将 Goroutine 放入一个就绪队列中,等待操作系统线程(M)来执行。
- 调度器会将一个 Goroutine 绑定到一个 P 上,而 P 负责将 Goroutine 分配给 M 来执行。
- P 通过选择一个 M 来执行任务。每个 P 都有自己的 Goroutine 队列,P 会从队列中选择一个 Goroutine 给 M 执行。
- M 和 P 的关系:
- 一个 M 只会在一个 P 上执行,它们是绑定关系。
- M 会执行 P 中调度的任务(即运行 Goroutine)。
- 每个 M 都会持有一个 Goroutine,并且会将其调度到对应的 P 上执行,直到该任务完成。
- 工作窃取:
- 如果某个 P 的 Goroutine 队列为空,它可以从其他 P 的队列中窃取一些 Goroutine 任务来执行,这样可以避免某些 P 的空闲,从而提高 CPU 的利用率。
- Goroutine 迁移和执行:
- 当一个 Goroutine 执行结束后,M 可能会选择一个新的 Goroutine 进行执行,或者在没有 Goroutine 时,M 会休眠等待新的 Goroutine 被调度。
- 如果 P 没有足够的任务,它可以选择将其与另一个 P 合并或将 M 从该 P 上移除,直到有新的任务。
关键点总结
- Goroutine (G):是 Go 程序并发的基本单位,相比传统线程,它轻量且开销低,通常一个程序会创建成千上万的 Goroutine。
- Machine (M):是 Go 的操作系统线程,用于执行任务,它负责将 Goroutine 的任务真正地派发给操作系统线程进行执行。
- Processor (P):是 Go 的逻辑处理器,负责调度 M 执行 Goroutine。
- 调度器:Go 运行时调度器负责管理 G、M、P 之间的协调与调度,保证 Goroutine 可以高效地执行。
为什么 Go 选择 GMP 模型?
- 高效的调度:
- 由于 Goroutine 是用户级线程,它的创建和切换开销非常低,可以快速启动大量并发任务。
- 调度器根据 CPU 核心数和系统负载动态调度 M 和 P,保证了最大化的 CPU 利用率。
- 支持高并发:
- Go 的调度器允许在多个 P 上同时调度多个 M 执行 Goroutine,这样可以在多核 CPU 上有效地运行成千上万的 Goroutine。
- 由于调度器的高效性,Go 可以在处理大量并发请求时,保持良好的性能。
- 避免阻塞:
- 由于多个 P 之间可以交换 M,Go 语言能够处理 I/O 密集型任务或其他阻塞任务,在一个 P 上的任务阻塞不会影响其他 P 的执行。
- 这使得 Go 在处理大量 I/O 操作时非常高效,例如在 Web 服务器中,多个 Goroutine 可以并发处理 HTTP 请求,即便某些请求在等待网络响应。
示例:GMP 模型的工作流程
下面是一个简单的示例,展示了 Go 中并发执行的基本过程:
package main
import (
"fmt"
"time"
)
func main() {
// 启动多个 Goroutine
for i := 0; i < 5; i++ {
go func(i int) {
fmt.Printf("Goroutine %d started\n", i)
time.Sleep(time.Second)
fmt.Printf("Goroutine %d finished\n", i)
}(i)
}
// 等待所有 Goroutine 执行完
time.Sleep(2 * time.Second)
}
- 在这个示例中,我们启动了 5 个 Goroutine。
- 每个 Goroutine 都执行一定的任务并打印消息。
- 调度器会将这些 Goroutine 分配到多个 P 上,然后通过 M 来执行它们。
- 程序通过
time.Sleep
等待 Goroutine 执行完毕。
GMP 模型的优势与局限
优势:
- 轻量级并发:由于 Goroutine 是用户级线程,它比操作系统级线程要轻量得多,开销小,能够启动成千上万个并发任务。
- 高效调度:通过 M、P 和 G 的调度模型,Go 能够高效地利用多核 CPU,避免线程创建和切换的系统开销。
- 适合高并发场景:Go 特别适用于 Web 服务器、网络 I/O 密集型应用等高并发场景,能够高效处理大量并发任务。
局限:
- 内存消耗:虽然 Goroutine 很轻量,但它们每个也需要一定的内存(例如栈空间),大量的 Goroutine 可能会消耗较大的内存。
- 调度复杂度:在高负载情况下,调度器的调度效率可能会成为瓶颈,尤其是在处理极大量的任务时。
总结
Go 的 GMP 模型是 Go 语言并发编程的核心,通过轻量级的 Goroutine 和灵活高效的调度器,实现了高效的并发任务处理。GMP 模型使得 Go 在处理大量并发请求时,比传统线程模型更加高效,特别适用于 Web 服务、微服务等高并发的场景。