Go 并发模型深入
Go 并发模型深入
一句话:Go 的并发哲学是 CSP(Communicating Sequential Processes)——“不要通过共享内存来通信,要通过通信来共享内存”。goroutine + channel 把”等待 I/O 时不阻塞其他任务”这件难事做成了默认行为。
本文是
Go 基础速查的深入版。速查讲”怎么用”,本文讲”为什么这样设计、怎么避免翻车”。
G-M-P 调度模型(理解一切的底座)
Go 调度器在用户态把 goroutine 映射到系统线程,三层:
graph TD
G1[G: goroutine] --> M1[M: OS 线程]
G2[G] --> M1
G3[G] --> M2
M1 --> P1[P: 逻辑处理器]
M2 --> P2[P]
P1 --> CPU1[CPU 核]
P2 --> CPU2[CPU 核]
- G(Goroutine):轻量协程,初始栈 2KB,按需增长。创建成本远低于线程
- M(Machine):操作系统线程,真正跑代码的实体
- P(Processor):逻辑处理器,持有可运行 G 的本地队列,GOMAXPROCS 决定 P 的数量(默认 = CPU 核数)
关键点:
- M 必须绑定 P 才能跑 G——P 是调度单元,决定并行度
- 本地队列 + 全局队列 + 工作窃取:P 本地空了去别的 P 偷,保证负载均衡
- syscall 阻塞时 M 与 P 解绑:一个 G 陷入系统调用,M 带着它脱离 P,P 立刻找另一个 M 继续跑其他 G——这是 Go “等待 I/O 不阻塞别人”的真相
这直接解释了你
Go 基础速查那句”goroutine 不是因为要并行才用,是因为要在等一个东西时不阻塞另外一千个东西”——调度器在 syscall 时自动把 P 让出来。
channel:并发通信的主干
| 类型 | 语义 | 陷阱 |
|---|---|---|
| 无缓冲 | 发送接收同时交接(同步点) | 单方先到会阻塞,易 deadlock |
| 有缓冲 | 缓冲未满/未空时不阻塞 | 缓冲满仍阻塞;缓冲泄漏(没人读) |
close | 关闭后读返回零值 + ok=false | 向已关闭 channel 发送 panic;重复 close panic |
常用模式
// 1. Worker Pool:控制并发度(N 个 worker 消费任务)
jobs := make(chan Task, 100)
for i := 0; i < N; i++ {
go func() {
for job := range jobs { process(job) } // range 自动在 channel 关闭后退出
}()
}
// 2. Fan-out / Fan-in:多 goroutine 处理,再合并结果
// 3. Pipeline:channel 串联多个 stage
// 4. select:多 channel 多路复用 + default 非阻塞
select {
case msg := <-ch1:
handle(msg)
case <-ctx.Done(): // 关键:用 context 控制退出
return
default:
// 非阻塞尝试
}
context:取消传播的命脉
context 解决”一个请求派生出多个 goroutine,怎么统一取消、怎么设超时”。
ctx, cancel := context.WithTimeout(parent, 3*time.Second)
defer cancel() // 必须调用,否则泄漏
go func() {
select {
case <-ctx.Done(): // 超时/取消时 Done() 关闭
return
case res := <-work:
deliver(res)
}
}()
WithCancel/WithTimeout/WithDeadline/WithValue- 铁律:
cancel()必须被调用(常配合defer);goroutine 必须监听<-ctx.Done()否则泄漏 - 你
HTTP 与 WebSocket 连接模型与超时里讲的”首包卡死”问题,本质就是没有 context 超时预算——Go HTTP client 的context.WithTimeout正是解法
sync 原语(共享内存的少数场景)
| 原语 | 用途 |
|---|---|
sync.Mutex / RWMutex | 临界区保护(写少读多用 RWMutex) |
sync.WaitGroup | 等一组 goroutine 结束 |
sync.Once | 单次初始化(如单例) |
sync.Pool | 对象复用,降 GC 压力(见 Go 性能调优) |
sync.Map | 高并发读写 map(读多写少,避免锁) |
并发四大陷阱(排障必看)
| 陷阱 | 现象 | 检测/解法 |
|---|---|---|
| Data Race | 偶发数据错乱 | go run -race 检测;用 channel 或锁 |
| Goroutine 泄漏 | 内存/协程数只涨不跌 | 监听 ctx.Done();pprof goroutine 看栈 |
| Deadlock | 全部阻塞、程序卡死 | 无缓冲 channel 单方等待;加超时 |
| Context 不取消 | 请求取消后后台仍跑 | 所有派生 goroutine 必须接 ctx |
关联知识
- Go 基础速查 — 本文的入门铺垫(模块/语法)
- Go 性能调优 — goroutine 泄漏用 pprof 定位
- HTTP 与 WebSocket 连接模型与超时 — context 超时是首包卡死的解法
- Claude Code 逆向 — agent loop 的并发编排思想同源
- Go 网络与 gRPC — net/http 的超时基于 context
参考资源
- Go Blog: “The Go scheduler” / “Go Concurrency Patterns”
- Russ Cox 文章:Go 调度器设计
学习时间
| 内容 | 日期 | 状态 |
|---|---|---|
| Go 并发模型 | 2026-07-24 | 完成:G-M-P、channel 模式、context、sync 原语、四大陷阱 |
状态
- G-M-P 调度
- channel 模式与陷阱
- context 取消传播
- sync 原语
- 并发四大陷阱