GPM
Go Goroutine 模型
Goroutine 是 Go 并发编程的核心概念之一。它是一种轻量级的线程,由 Go 运行时(Go runtime)管理,而不是直接由操作系统管理。Goroutine 在 Go 中用于执行并发任务,并能够通过 Go 调度器高效地调度执行。
1. Goroutine 基本概念
- Goroutine 是 Go 程序中的轻量级线程,使用
go关键字启动。 - 每个 Goroutine 都有自己的栈,初始时栈非常小(大约 2 KB),并会根据需要动态扩展或收缩。
- Goroutine 由 Go 调度器管理,调度器负责将 Goroutine 映射到操作系统线程上。
2. Goroutine 调度模型(GPM 模型)
Go 使用一种叫做 GPM(Goroutine、Processor、Machine)的调度模型。该模型通过 Go 调度器管理 Goroutine 的执行。
- G(Goroutine):是 Go 程序中的最小执行单位,每个 Goroutine 表示一个独立的任务。
- P(Processor):处理器,负责执行 Goroutine,调度 Goroutine 到 M 上执行。每个 P 可以调度多个 G,P 的数量通常设置为 CPU 核心数(
GOMAXPROCS)。 - M(Machine):机器,表示操作系统线程。Go 调度器将 P 和 M 关联起来,将 Goroutine 分配到 M 上执行。
GPM 调度模型图示:
+---------+ +---------+ +---------+
| G | | G | | G |
| Goroutine | --> Goroutine | --> Goroutine |
+---------+ +---------+ +---------+
| | |
+----v----+ +----v----+ +----v----+
| P | | P | | P |
| Processor | | Processor | | Processor |
+----+----+ +----+----+ +----+----+
| | |
+----v----+ +----v----+ +----v----+
| M | | M | | M |
| OS Thread | | OS Thread | | OS Thread |
+-------------+ +-------------+ +-------------+- 每个 P 有一个本地队列用于存储待执行的 Goroutine(G)。
- 每个 M 是一个实际的操作系统线程,执行 P 分配给它的任务。
- Go 调度器会动态地调度 Goroutine 到不同的 M 上执行。
3. Goroutine 的特点
- 轻量级:Goroutine 相比操作系统线程非常轻量,一个程序可以同时创建数百万个 Goroutine,而不会造成太大的性能开销。
- 自动扩展栈:Goroutine 初始时栈非常小(约 2 KB),随着 Goroutine 执行的需要,栈大小会动态增长,最大可扩展到 1 MB。
- 高效调度:Go 调度器会根据 Goroutine 的数量和系统资源动态调整执行,最大限度地利用多核处理器。
4. Goroutine 的生命周期
- 创建:通过
go关键字启动 Goroutine,例如go f()。 - 执行:Goroutine 会在调度器的控制下开始执行。
- 阻塞/等待:Goroutine 在等待 IO 操作、接收通道数据或进行其他阻塞操作时会被挂起。
- 终止:Goroutine 完成任务后会退出,Go 运行时会回收资源。
5. Goroutine 的调度
- Go 的调度器是基于抢占式调度的,允许 Goroutine 在 CPU 上共享时间片。
- 调度器会定期抢占正在执行的 Goroutine,并将 CPU 时间片分配给其他 Goroutine。
- 调度器通过监控 Goroutine 的状态来决定哪个 Goroutine 应该执行。例如,如果一个 Goroutine 在等待通道数据,调度器会将它挂起,并激活其他准备好的 Goroutine。
6. Goroutine 与操作系统线程
- 每个操作系统线程(M)可以同时执行多个 Goroutine。
- Goroutine 是由 Go 调度器管理的,并不直接映射到操作系统线程。调度器可以根据需要将 Goroutine 重新调度到不同的线程上执行。
7. Goroutine 示例
go
package main
import "fmt"
// 一个简单的 Goroutine 示例
func sayHello() {
fmt.Println("Hello from Goroutine!")
}
func main() {
go sayHello() // 启动一个 Goroutine
fmt.Println("Hello from main!")
}输出(执行顺序可能不同):
Hello from main!
Hello from Goroutine!8. Goroutine 与 Channel 的协作
Goroutine 通常与 Channel 一起使用,以便 Goroutine 之间进行通信。例如,通过 Channel 向其他 Goroutine 发送结果或状态。
go
package main
import "fmt"
func sum(a, b int, ch chan int) {
result := a + b
ch <- result // 将结果发送到通道
}
func main() {
ch := make(chan int)
go sum(1, 2, ch) // 启动 Goroutine
result := <-ch // 从通道接收结果
fmt.Println("Result:", result)
}输出:
Result: 39. Goroutine 使用注意事项
- 资源管理:Goroutine 创建后需要合理管理,避免泄漏。使用
sync.WaitGroup或select等机制来确保 Goroutine 执行完毕。 - 并发问题:当多个 Goroutine 访问共享资源时,需要使用同步原语(如
sync.Mutex)来避免数据竞争。 - 死锁:多个 Goroutine 在等待互相持有的资源时可能导致死锁,设计时应避免这种情况。
总结
Goroutine 是 Go 的并发编程核心,通过调度器高效地管理。它的轻量级特性和高效的调度机制使得 Go 在并发场景中非常适合处理大量并行任务。通过与 Channel、WaitGroup 等工具结合使用,Goroutine 可以实现更高效、更可靠的并发处理。