Go GMP 调度器与设计哲学
对应视频 9-2 go语言的调度器、18-1 体会Go语言的设计、18-2 课程总结
1. Go 调度器演进
1.0 时代:单线程调度器(Go 0.x)
- 只有一个线程运行 goroutine
- 所有 goroutine 排队等待
- 无法利用多核
1.1 时代:多线程调度器(Go 1.0)
- 引入多线程
- 但全局锁竞争严重,性能瓶颈
1.2+ 时代:GMP 模型(Go 1.2 至今)
引入了著名的 GMP 调度模型。
2. GMP 模型详解
G (Goroutine) - 协程,用户级轻量线程
M (Machine/Thread) - 操作系统线程
P (Processor) - 逻辑处理器,调度上下文
2.1 三者的关系
┌─────────┐
│ Go 程序 │
└────┬────┘
│ 创建 goroutine
┌────────────┼────────────┐
▼ ▼ ▼
┌─────┐ ┌─────┐ ┌─────┐
│ G │ │ G │ │ G │ ... (成千上万个)
└──┬──┘ └──┬──┘ └──┬──┘
│ │ │
┌─────┴─────┐ │ ┌────┴─────┐
│ P 的本地队列│ │ │ P 的本地队列│
│ [G][G][G] │ │ │ [G][G] │
└─────┬─────┘ │ └────┬─────┘
│ │ │
┌──┴──┐ ┌──┴──┐ ┌──┴──┐
│ P │ │ P │ │ P │ (GOMAXPROCS 个)
└──┬──┘ └──┬──┘ └──┬──┘
│ │ │
┌──┴──┐ ┌──┴──┐ ┌──┴──┐
│ M │ │ M │ │ M │ (按需创建)
└──┬──┘ └──┬──┘ └──┬──┘
│ │ │
══════╧═══════════╧══════════╧══════
操作系统内核线程
2.2 各组件详解
G (Goroutine)
- 初始栈大小仅 2KB(线程通常 1-8MB)
- 栈可动态增长和收缩
- 包含执行的函数、栈指针、程序计数器等
- 状态:可运行、运行中、等待中、已完成
M (Machine)
- 对应一个操作系统线程
- 由 Go runtime 按需创建,默认最多 10000 个
- M 必须持有 P 才能运行 G
- 当 M 因系统调用阻塞时,P 会被抢走给其他 M
P (Processor)
- 数量由 GOMAXPROCS 决定(默认等于 CPU 核数)
- 每个 P 有一个本地运行队列(最多 256 个 G)
- P 是调度的核心,决定哪个 G 在哪个 M 上运行
2.3 调度策略
// 查看和设置 P 的数量
runtime.GOMAXPROCS(0) // 获取当前值
runtime.GOMAXPROCS(4) // 设置为 4
runtime.NumCPU() // CPU 核数
调度时机(goroutine 可能的切换点):
| 切换点 | 说明 |
|---|---|
| I/O 操作 | 文件、网络读写 |
| channel 操作 | 发送/接收阻塞时 |
| select | 多路复用 |
| 等待锁 | sync.Mutex 等 |
| 函数调用 | 编译器在函数入口插入检查点 |
runtime.Gosched() |
手动让出 |
| GC | 垃圾回收的 STW 阶段 |
| 系统调用 | syscall 阻塞时 M 和 P 分离 |
2.4 Work Stealing(工作窃取)
当一个 P 的本地队列为空时:
- 先从全局队列取 G
- 全局队列也没有 → 随机从其他 P 的队列偷一半 G
- 都没有 → 检查网络轮询器
- 还没有 → M 休眠
P1 [空] P2 [G5 G6 G7 G8]
│ │
│ 偷一半! │
│ ◄───── steal ───── │
│ │
P1 [G7 G8] P2 [G5 G6]
2.5 系统调用处理(Hand Off)
当 G 执行系统调用(如文件 I/O)阻塞 M 时:
正常状态: M1 ──── P1 ──── G1(syscall阻塞)
Hand Off: M1 ──── G1(继续阻塞在syscall)
M2 ──── P1 ──── G2(P被转给新的M)
P 会被转给空闲的 M(或新建一个 M),不让 P 闲着。
2.6 网络轮询器(netpoller)
网络 I/O 不会阻塞 M,而是使用 epoll/kqueue 异步处理:
- G 发起网络调用 → G 被挂到 netpoller
- M 不阻塞,继续运行其他 G
- 网络就绪时 → G 被放回可运行队列
这就是为什么 Go 能高效处理大量网络连接。
3. Goroutine 与线程的对比
| 特性 | Goroutine | OS Thread |
|---|---|---|
| 栈大小 | 2KB(动态增长) | 1-8MB(固定) |
| 创建成本 | ~0.3μs | ~30μs |
| 切换成本 | ~0.2μs(用户态) | ~1μs(内核态) |
| 调度方式 | 协作式+抢占式 | 抢占式 |
| 数量级 | 百万级 | 千级 |
| 通信方式 | channel | 共享内存+锁 |
抢占式调度(Go 1.14+)
Go 1.14 之前是纯协作式调度,密集计算的 goroutine 可能长时间占用 M:
// Go 1.14 之前,这个会独占线程
go func() {
for {} // 死循环,不让出
}()
Go 1.14+ 引入了基于信号的抢占(SIGURG),即使没有函数调用也能被抢占。
4. 实际观察调度器
# GODEBUG 查看调度器状态
GODEBUG=schedtrace=1000 go run main.go
# 每 1000ms 输出调度器状态
# 更详细
GODEBUG=schedtrace=1000,scheddetail=1 go run main.go
# go tool trace 可视化
// 在代码中查看
fmt.Println("goroutine数:", runtime.NumGoroutine())
fmt.Println("CPU核数:", runtime.NumCPU())
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))
5. Go 语言的设计哲学
对应视频 Ch18:体会Go语言的设计
5.1 少即是多(Less is more)
- 只有 25 个关键字(C 有 32 个,Java 有 50+)
- 没有 class,用 struct + method 替代
- 没有继承,用组合(embedding)替代
- 没有泛型(Go 1.18 前)—— 保持简单
- 没有异常,用多返回值 + error 替代 try/catch
- 只有 for 循环,没有 while/do-while
- 没有三元运算符
?:
5.2 组合优于继承
// 不是继承,是组合
type Animal struct {
Name string
}
func (a Animal) Speak() string { return a.Name + " speaks" }
type Dog struct {
Animal // 嵌入,不是继承
Breed string
}
// Dog 自动获得 Speak 方法,但可以覆盖
5.3 接口的隐式实现
// 不需要 implements 关键字
type Stringer interface {
String() string
}
// 任何有 String() string 方法的类型自动实现了 Stringer
// → 解耦了定义者和实现者
5.4 并发是一等公民
go关键字启动协程(不是库函数)chan是内置类型(不是库中的 Queue)select是语言级别的多路复用- "Don't communicate by sharing memory; share memory by communicating."
5.5 工具链哲学
| 工具 | 作用 |
|---|---|
gofmt |
统一代码风格,没有风格战争 |
go vet |
静态分析,找常见错误 |
go test |
内置测试框架,不需要第三方 |
go doc |
注释即文档 |
go build |
编译成单一二进制,无依赖 |
go mod |
内置依赖管理 |
5.6 错误处理哲学
// 显式处理每个错误,不会被隐藏
f, err := os.Open("file.txt")
if err != nil {
// 必须处理
}
// 虽然"啰嗦",但:
// 1. 错误路径清晰可见
// 2. 不会因为忘记 catch 而崩溃
// 3. 强迫你思考每个可能的失败
5.7 Go 箴言(Go Proverbs by Rob Pike)
| 箴言 | 含义 |
|---|---|
| Don't communicate by sharing memory, share memory by communicating | 用 channel 而非锁 |
| Concurrency is not parallelism | 并发是结构,并行是执行 |
| Channels orchestrate; mutexes serialize | channel 编排流程,mutex 串行化访问 |
| The bigger the interface, the weaker the abstraction | 接口越小越好 |
| Make the zero value useful | 零值应该可直接使用 |
| interface{} says nothing | 空接口不表达任何信息 |
| Gofmt's style is no one's favorite, yet gofmt is everyone's favorite | 统一风格比好看重要 |
| A little copying is better than a little dependency | 少量复制好过引入依赖 |
| Clear is better than clever | 清晰胜过聪明 |
| Errors are values | 错误是值,可以编程处理 |
| Don't just check errors, handle them gracefully | 优雅处理错误,不只是检查 |
| Don't panic | 不要轻易 panic |
6. 课程总结思维导图
Go 语言核心
├── 基础语法
│ ├── 变量、常量、类型
│ ├── 控制流(if/for/switch)
│ └── 函数(多返回值、闭包、defer)
├── 面向对象
│ ├── struct + method(代替 class)
│ ├── 组合(代替继承)
│ └── interface(隐式实现、duck typing)
├── 函数式编程
│ ├── 闭包、高阶函数
│ ├── 装饰器/中间件模式
│ └── Functional Options
├── 错误处理
│ ├── error 接口、多返回值
│ ├── panic/recover(仅用于不可恢复错误)
│ └── 统一错误处理(errWrapper 模式)
├── 测试
│ ├── 表格驱动测试
│ ├── 性能测试(Benchmark)
│ ├── pprof 性能分析
│ └── Example 文档测试
├── 并发编程
│ ├── goroutine(GMP 调度模型)
│ ├── channel(CSP 通信模型)
│ ├── select(多路复用)
│ └── sync 包(WaitGroup、Mutex)
├── 标准库
│ ├── net/http
│ ├── encoding/json
│ └── html/template
└── 工程实践
├── 单任务→并发→分布式爬虫
├── ElasticSearch 集成
├── Docker 容器化
└── RPC 分布式通信
主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://walker-learn.xyz/archives/6752