应从 mutex.go、waitgroup.go、once.go 三个文件开始读 sync 包源码,它们是 Go 并发原语的核心实现,代码精简且逻辑清晰;避免过早深入 runtime 或 atomic 包。

从哪几个文件开始读 sync 包源码
直接打开 $GOROOT/src/sync 目录,核心就三个文件: mutex.go、waitgroup.go、once.go —— 其它如 pool.go、map.go 是衍生实现,先别碰。Go 官方把最基础的并发原语压得极薄,mutex.go 不到 400 行,waitgroup.go 更短,这才是你该盯死的第一现场。
常见错误现象:
– 一上来就跳进 runtime/sema.go 或 runtime/proc.go,结果卡在调度器细节里出不来
– 把 atomic 包当成 sync 的一部分去读,其实它是底层支撑,不是 sync 的逻辑主体
-
mutex.go:重点看Lock()的快速路径(CAS)和慢速路径(lockSlow()),再对照注释里写的自旋条件(runtime_canSpin)、饥饿阈值(1ms) -
waitgroup.go:关键在state1数组怎么用一个uint64原子变量塞进 counter + waiter + sema 指针,Add()里那句atomic.AddUint64(statep, uint64(delta) 就是全部秘密 -
once.go:只看Do()里那个atomic.LoadUint32(&o.done)和atomic.CompareAndSwapUint32(&o.done, 0, 1)怎么配合mu.Lock()构成双重保险
读源码时必须盯住的字段和状态位
sync 类型不是黑盒,每个 struct 都明确定义了状态编码方式。不搞清这些位,等于没读。
比如 Mutex.state 是 int32,但低 3 位是标志位:
– mutexLocked = 1(第 0 位)
– mutexWoken = 2(第 1 位)
– mutexStarving = 4(第 2 位)
高位存 waiter 计数。你看到 atomic.AddInt32(&m.state, -mutexLocked),就得反应过来:这是在清锁位,不是简单减 1。
-
WaitGroup.state1是[3]uint32,但实际只用前两个:state1[0]存 counter(高 32 位),state1[1]存 waiter(低 32 位),state1[2]是 sema 地址 —— 这个布局决定了为什么Add(-1)会 panic:counter 溢出变负,int32(state)就小于 0 -
Once.done是uint32,只用 0/1 两个值,Do()里所有分支都围绕它是否为 1 展开,没有第三种状态
哪些代码千万别照抄到业务里
sync 包里大量使用 unsafe.Pointer、runtime_procPin()、runtime_Semacquire() 等运行时接口,这些不是给你调的。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
常见错误现象:
– 在自己代码里手动调 runtime_Semacquire 想模仿 WaitGroup 的阻塞逻辑 → 编译失败或 panic
– 把 Pool.local 的 unsafe.Pointer 强转成 []poolLocal 来遍历 → 地址错乱,GC 时崩溃
- 所有以
runtime_开头的函数,只在 runtime 包内部或 sync 包内合法使用,应用层禁止触碰 -
noCopy字段不是装饰,是编译期检查机制;如果你结构体嵌了sync.Mutex却又想 copy 实例,go vet会报illegal copy -
Pool.New返回的是interface{},但实际 Put/Get 的类型必须一致,否则.(*T)断言失败 —— 这不是源码问题,是你用错了
调试 sync 行为最有效的手段
别靠 print,也别迷信 pprof。真正管用的是 Go 自带的竞态检测器和调试标记。
- 加
-race运行:它能直接指出哪两个 goroutine 在争抢同一个Mutex字段,甚至标出sync/atomic和普通赋值混用的位置 - 设环境变量
GODEBUG=mutexprofile=1,然后用go tool trace打开 trace 文件,能看到每个Lock()耗时、是否自旋、是否进入饥饿模式 - 在
WaitGroup.Wait()前后打runtime.GC(),观察是否因 GC 导致 waiter 唤醒延迟(Pool 的 victim 机制就和这个强相关)
最容易被忽略的地方是:sync 包所有原语都不保证「公平」—— Mutex 不保证先等先得,WaitGroup 不保证 Add 和 Wait 的调用顺序可预测,Once 不提供失败重试。它们只保证「正确性」,不负责「可预期性」。你要的如果是顺序或超时,得自己包一层。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










