Go GMP调度模型
📚 第1课:Go GMP调度模型
1. G(Goroutine)
1.1 定义
- Goroutine是Go语言的轻量级线程
- 由Go运行时管理
- 用户态线程,不需要操作系统内核参与
1.2 特点
- 轻量级:初始栈大小只有2KB
- 快速创建:创建一个goroutine只需要几微秒
- 高效调度:由Go运行时调度,不需要内核参与
- 并发执行:多个goroutine可以并发执行
1.3 状态
新建 → 可运行 → 运行中 → 等待 → 可运行 → 运行中
↓
结束
状态说明:
_Gidle:刚创建,未初始化_Grunnable:可运行,等待被调度_Grunning:正在运行_Gsyscall:系统调用中_Gwaiting:等待中(如channel操作、sleep等)_Gdead:已结束
2. M(Machine)
2.1 定义
- Machine代表操作系统线程
- 每个M对应一个操作系统线程
- M负责运行goroutine
2.2 特点
- 绑定P:M必须绑定P才能运行goroutine
- 执行G:M从P的运行队列中获取G并执行
- 系统调用:M可以执行系统调用
- 自旋:M可以自旋等待工作
2.3 状态
- 运行中:正在执行goroutine
- 空闲:没有goroutine可执行
- 系统调用中:执行系统调用
- 自旋中:自旋等待工作
3. P(Processor)
3.1 定义
- Processor代表逻辑处理器
- P是G和M之间的桥梁
- P维护本地运行队列
3.2 特点
- 数量固定:默认等于CPU核心数,可通过
GOMAXPROCS设置 - 本地队列:每个P维护一个本地运行队列
- 全局队列:所有P共享一个全局运行队列
- 工作窃取:P可以从其他P窃取goroutine
3.3 状态
- 运行中:正在执行goroutine
- 空闲:没有goroutine可执行
- 系统调用中:执行系统调用
4. 三者关系
4.1 基本关系
P(处理器)
├─ 维护本地运行队列(存储G)
├─ 绑定M(执行G)
└─ 从全局队列获取G
M(线程)
├─ 绑定P
├─ 从P的队列获取G
└─ 执行G
G(goroutine)
├─ 存储在P的本地队列
├─ 或存储在全局队列
└─ 被M执行
4.2 数量关系
- G:可以无限创建
- P:默认等于CPU核心数
- M:通常等于P的数量,但可以更多
4.3 绑定关系
- M必须绑定P才能运行G
- 一个P同一时间只能绑定一个M
- 一个M同一时间只能绑定一个P
5. Goroutine调度流程
5.1 创建goroutine
1. 用户调用go func()
↓
2. 创建新的G
↓
3. 将G放入当前P的本地队列
↓
4. 如果本地队列满了,放入全局队列
5.2 调度goroutine
1. M从P的本地队列获取G
↓
2. 如果本地队列为空,从全局队列获取G
↓
3. 如果全局队列为空,从其他P窃取G
↓
4. M执行G
5.3 goroutine切换
1. G执行完成
↓
2. M从P的队列获取下一个G
↓
3. 如果没有G,M进入空闲状态
5.4 系统调用
1. G执行系统调用
↓
2. M和P解绑
↓
3. M执行系统调用
↓
4. 系统调用完成
↓
5. M重新绑定P
↓
6. 继续执行G
6. 代码示例
6.1 观察GMP调度
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
// 设置P的数量
runtime.GOMAXPROCS(4)
// 创建多个goroutine
for i := 0; i < 10; i++ {
go func(n int) {
fmt.Printf("Goroutine %d running\n", n)
time.Sleep(100 * time.Millisecond)
}(i)
}
time.Sleep(1 * time.Second)
}
6.2 观察工作窃取
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
runtime.GOMAXPROCS(4)
var wg sync.WaitGroup
// 在一个P上创建大量goroutine
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Printf("Goroutine %d\n", n)
}(i)
}
wg.Wait()
}
7. 关键要点
7.1 核心概念
- G(Goroutine):轻量级线程,由Go运行时管理
- M(Machine):操作系统线程,执行goroutine
- P(Processor):逻辑处理器,维护运行队列
7.2 调度特点
- M:N调度:M个线程映射到N个goroutine
- 抢占式调度:goroutine可以被抢占
- 工作窃取:P可以从其他P窃取goroutine
- 本地队列:每个P维护本地运行队列
7.3 性能优化
- 设置GOMAXPROCS:根据CPU核心数设置P的数量
- 避免阻塞:减少系统调用和阻塞操作
- 平衡负载:合理分配goroutine到不同P
7.4 调试工具
- runtime.NumGoroutine():查看goroutine数量
- runtime.GOMAXPROCS():设置P的数量
- runtime/pprof:性能分析
8. 常见问题
8.1 为什么需要GMP?
- 提高并发性能:避免内核态切换
- 减少资源消耗:goroutine比线程轻量
- 高效调度:用户态调度更灵活
8.2 GOMAXPROCS设置多少合适?
- 默认值:等于CPU核心数
- CPU密集型:可以设置为核心数
- IO密集型:可以设置为核心数的2倍
8.3 如何避免goroutine泄漏?
- 及时关闭channel
- 使用context取消goroutine
- 避免无限循环
9. 总结
9.1 GMP模型的核心
- G:轻量级线程
- M:操作系统线程
- P:逻辑处理器
9.2 调度流程
- 创建:创建G,放入P的队列
- 调度:M从P的队列获取G并执行
- 切换:G执行完成,切换到下一个G
- 系统调用:M和P解绑,系统调用完成重新绑定
9.3 性能优势
- 轻量级:goroutine比线程轻量
- 高效调度:用户态调度
- 工作窃取:平衡负载
- 并发执行:充分利用多核
10. 课后练习
10.1 练习1:观察goroutine数量
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
fmt.Printf("Initial goroutines: %d\n", runtime.NumGoroutine())
for i := 0; i < 10; i++ {
go func() {
time.Sleep(1 * time.Second)
}()
}
fmt.Printf("After creating: %d\n", runtime.NumGoroutine())
time.Sleep(2 * time.Second)
fmt.Printf("After GC: %d\n", runtime.NumGoroutine())
}
10.2 练习2:观察GOMAXPROCS影响
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func main() {
for procs := 1; procs <= 4; procs++ {
runtime.GOMAXPROCS(procs)
start := time.Now()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(10 * time.Millisecond)
}()
}
wg.Wait()
fmt.Printf("GOMAXPROCS=%d, Time=%v\n", procs, time.Since(start))
}
}