深入探讨 Go 语言中 goroutine协程调度 的底层实现与并发安全
"老王,为什么本文创建了 1000 个 goroutine,CPU 还是只用了 2 个核?" 新来的实习生小张一脸困惑。本文看了看他的代码,发现他没设置 GOMAXPROCS。"你这是把 GMP 调度模型给忘了啊!"GMP?什么是 GMP?看来得从基础讲起了。今天本文们聊聊 Go 的 GMP 调度模型。G 是 goroutine:轻量的用户态线程M 是线程:真正执行代码的操作系统线程P 是调度器:
深入探讨 Go 语言中 goroutine协程调度 的底层实现与并发安全
GMP 这个"三件套"本文研究了一周,终于搞明白 Go 调度是怎么回事了

前言
"老王,为什么本文创建了 1000 个 goroutine,CPU 还是只用了 2 个核?" 新来的实习生小张一脸困惑。
本文看了看他的代码,发现他没设置 GOMAXPROCS。"你这是把 GMP 调度模型给忘了啊!"
"GMP?什么是 GMP?"
看来得从基础讲起了。今天本文们聊聊 Go 的 GMP 调度模型。
一、底层原理
1.1 GMP 三件套
- G:goroutine,协程
- M:machine,操作系统线程
- P:processor,调度器
graph TD
A["G(Goroutine)"] --> B["运行"]
B --> C{"P 的数量?"}
C -->|有限| D["P(Processor)"]
D --> E["M(Machine/线程)"]
E --> F["CPU"]
G["全局运行队列"] --> D
H["G 阻塞"] --> I["M 让出 P"]
I --> J["P 绑定新 M"]
关键点:
- G 是逻辑上的"协程"
- M 是真正的操作系统线程
- P 是调度器,数量固定(GOMAXPROCS)
- G 要在 M 上跑,M 要有 P
- G 阻塞时,M 让出 P,P 绑定新 M
1.2 调度协作 vs 抢占
| 调度方式 | 优点 | 缺点 |
|---|---|---|
| 协作式 | 减少上下文切换 | 代码要主动让出 |
| 抢占式 | 不用代码配合 | 开额外开销 |
| Go 混合 | 兼顾 | 实现复杂 |
Go 1.14 之后实现了基于信号的抢占式调度,但仍然保留了协作式的特征。
二、快速上手
看个例子,理解调度本质:
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
runtime.GOMAXPROCS(2) // 用 2 个 P
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("G %d 在 P 上跑\n", id)
}(i)
}
wg.Wait()
fmt.Println("全部完成")
}
用 GOMAXPROCS 控制 P 的数量,可以看出"同一时刻并行执行的任务数"。
开启 tracing:
import "runtime/trace"
func main() {
f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()
// ... 业务代码
}
然后 go tool trace trace.out 就能看到调度情况。
三、核心 API / 深水区
3.1 GMP 相关参数速查
| 参数 | 作用 | 建议 |
|---|---|---|
| GOMAXPROCS | P 的数量 | 一般等于 CPU 核数 |
| 全局队列 | 待调度的 G | 调度器自动管理 |
| 本地队列 | P 私有的 G | 优先调度 |
| 工作窃取 | P 偷其他 P 的 G | 自动触发 |
3.2 GOMAXPROCS 怎么设
// 看 CPU 核数
fmt.Println(runtime.NumCPU())
// 设置 P 的数量
runtime.GOMAXPROCS(4)
// 一般默认就够
// runtime.GOMAXPROCS(0) 返回当前值
3.3 工作窃取原理
当一个 P 的本地队列空了,它会去其他 P 的队列"偷"一半 G 过来。这能充分利用多核:
// 演示工作窃取的效果
func workStealingDemo() {
runtime.GOMAXPROCS(4)
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 1000000; j++ {
_ = j * j
}
}()
}
wg.Wait()
}
四、实战演练
模拟 I/O 阻塞时的调度:
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func main() {
runtime.GOMAXPROCS(2)
var wg sync.WaitGroup
// CPU 密集型
for i := 0; i < 4; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 5; j++ {
for k := 0; k < 10000000; k++ {
_ = k * k
}
fmt.Printf("G %d CPU 完成第 %d 轮\n", id, j)
}
}(i)
}
// I/O 密集型(模拟)
for i := 10; i < 14; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 5; j++ {
time.Sleep(10 * time.Millisecond) // 模拟 I/O
fmt.Printf("G %d I/O 完成第 %d 轮\n", id, j)
}
}(i)
}
wg.Wait()
}
注意:I/O 阻塞时,G 让出 P,其他 G 就能跑。
五、避坑指南与最佳实践
💡 **技巧:GOMAXPROCS 不是越大越好
P 多了调度开销也大,通常是 CPU 核数或者少一点。
⚠️ **警告:不要创建太多 goroutine
虽然轻量,但 10 万个也比 100 个重。用 goroutine pool。
✅ **推荐:用 channel 做工作池
控制并发数量,防止 goroutine 爆炸。
六、综合实战演示
并发控制工作池:
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
type WorkerPool struct {
maxWorkers int
tasks chan func()
wg sync.WaitGroup
}
func NewWorkerPool(max int) *WorkerPool {
pool := &WorkerPool{
maxWorkers: max,
tasks: make(chan func(), 1000),
}
pool.start()
return pool
}
func (p *WorkerPool) start() {
for i := 0; i < p.maxWorkers; i++ {
p.wg.Add(1)
go p.worker(i)
}
}
func (p *WorkerPool) worker(id int) {
defer p.wg.Done()
for task := range p.tasks {
fmt.Printf("工人 %d 开始工作\n", id)
task()
}
}
func (p *WorkerPool) Submit(task func()) {
p.tasks <- task
}
func (p *WorkerPool) Stop() {
close(p.tasks)
p.wg.Wait()
}
func main() {
runtime.GOMAXPROCS(4)
pool := NewWorkerPool(4)
for i := 0; i < 100; i++ {
n := i
pool.Submit(func() {
time.Sleep(10 * time.Millisecond)
fmt.Printf("任务 %d 完成\n", n)
})
}
pool.Stop()
fmt.Println("全部完成")
}
七、总结
GMP 调度是 Go 并发的基础:
- G 是 goroutine:轻量的用户态线程
- M 是线程:真正执行代码的操作系统线程
- P 是调度器:连接 G 和 M 的桥梁
- 阻塞时 G 让出 P:让其他 G 有机会执行
- 工作窃取:P 之间互相偷 G,充分利用多核
理解了 GMP,写并发代码心里就有底了。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)