深浅色
Go 并发与运行时(背诵版)
1. GMP 模型
- G:goroutine,用户态协程,初始栈 2KB 可增长。
- M:内核线程,真正干活的执行体。
- P:逻辑处理器,持有本地队列(256 个 G),数量 =
GOMAXPROCS(默认 CPU 核数)。 - 调度流程:
go f()建 G 放本地队列 -> 本地空时从全局队列取或从别的 P 偷(work stealing) -> 每 61 次调度检查一次全局队列防饿死。 - 阻塞处理:G 进系统调用时 M 与 P 解绑(handoff),P 交给别的 M 继续跑,不浪费核。
- 追问:为什么 P 默认等于核数?-> 超过核数会带来额外上下文切换,收益递减。
2. 调度时机与抢占
- 调度点:函数调用(栈增长检查)、channel 操作、
go语句、GC、系统调用返回、runtime.Gosched()。 - 1.14 前是协作式,没有函数调用的死循环会卡住整个 P。
- 1.14 引入基于信号的异步抢占(
SIGURG),能打断长时间运行的循环。
3. channel 底层
hchan= 环形缓冲buf+sendx/recvx+ 等待队列sendq/recvq+ 一把锁。- 无缓冲:发送方直接把值交给等待的接收方,不经缓冲。
- 有缓冲:满了则发送方进
sendq阻塞。 - close 后仍可读完剩余数据,之后返回零值 + ok=false;向已关闭 channel 发送 panic,重复 close 也 panic。
select多个就绪时随机选;default实现非阻塞。- 追问:nil channel?-> 读写永久阻塞;select 里永不就绪,可用来动态禁用分支。
4. Mutex
- 状态位:locked / woken / starving + 等待计数。
- 正常模式:等待者 FIFO,但新来的可以自旋插队,吞吐好但可能饿死等待者。
- 饥饿模式:等待超过 1ms 触发,锁直接交给队首,新来的不自旋,保证公平。
RWMutex坑:不可重入;读锁里再取读锁,在写锁等待时可能死锁。- 追问:Mutex 能重入吗?-> 不能,重复 Lock 直接死锁。
5. WaitGroup / Once / atomic
WaitGroup:Add必须在Wait之前(或至少在启 goroutine 前),不能复制(含 noCopy)。Once:保证只执行一次;f panic 了也算执行过,不会重试。atomic:1.19+ 用atomic.Int64等类型更安全;CAS 循环实现无锁更新。- 追问:atomic 能替代锁吗?-> 只保护单个变量;多变量的一致性还得靠锁。
6. sync.Map
- 适用:读多写少、key 集合相对稳定(注册表、只增不减的缓存)。
- 实现:
read(atomic,无锁读)+dirty(加锁)双层,命中 read 时完全无锁。 - 不适用:频繁增删改的通用场景,性能与
map + Mutex差不多甚至更差。 - 追问:为什么没有
len()?-> 两层结构统计成本高,只能Range遍历。
7. happens-before 与可见性
- Go 不保证多 goroutine 间写入立即可见,必须用同步原语建立 happens-before。
- 建立手段:channel 收发、Mutex 的 Unlock->Lock、atomic 操作、WaitGroup 的 Done->Wait、Once。
- 经典错误:锁外读共享变量、用
time.Sleep代替同步。 - 追问:Mutex 能保证临界区的写对下一个持锁者可见吗?-> 能,Unlock 到 Lock 建立了关系。
8. GC
- 并发三色标记 + 混合写屏障(1.8+)。白 = 未标记,灰 = 待处理,黑 = 已处理完。
- 写屏障保证 GC 期间的新对象和指针变更不漏标。
- 触发:
GOGC(默认 100,堆翻倍触发);1.19+ 的GOMEMLIMIT设内存软上限。 - STW 只在标记开始和结束的瞬间,微秒级。
- 追问:怎么降 GC 压力?-> 减少堆分配(对象复用、sync.Pool)、调 GOGC/GOMEMLIMIT、少造小对象。
9. pprof
- 引入
net/http/pprof暴露/debug/pprof/;或runtime/pprof写文件。 - 常用端点:
/profile(CPU 30s)、/heap(内存)、/goroutine?debug=2(栈,查泄漏)。 - 分析:
go tool pprof -http=:8080 <binary> <profile>看 top 和火焰图。 - goroutine 泄漏特征:数量随时间单调上升,栈都停在 channel 收发或 select。
10. 常用并发模式
errgroup:任一 goroutine 出错就取消 ctx,Wait()返回第一个错误。singleflight:相同 key 的并发请求只打一次下游,其余共享结果(防击穿)。- worker pool:固定 worker 数从 channel 取任务,控制并发度。
- 限流:
golang.org/x/time/rate(令牌桶)。 - 追问:errgroup 与 WaitGroup 的区别?-> 前者能传播错误并取消,后者只能等。
11. goroutine 泄漏
- 常见原因:向无人接收的 channel 发送、从无人发送的 channel 接收、select 缺
ctx.Done()分支、HTTP body 没 close、循环里堆积time.After。 - 排查:
/debug/pprof/goroutine?debug=2,找同一处阻塞的大量 goroutine。 - 预防:每个会阻塞的 goroutine 都要有退出路径(ctx、done channel、超时)。
- 追问:卡在 channel 上的 goroutine 能被 GC 吗?-> 不能,它可达,必须让它退出。