sync.Cond多goroutine场景下死锁问题排查与优化
解决sync.Cond Broadcast导致的死锁问题及定位方法
死锁原因
当前代码的核心问题是Broadcast执行时机过早:
- 启动三个subscribe goroutine后,main立刻调用
Broadcast,但此时goroutine可能还没完成加锁、进入Wait状态,导致通知直接被“错过”。 - 后续subscribe goroutine执行到
Wait时,会一直等待通知,但不会再有新的Broadcast触发,三个goroutine永久阻塞;同时main里的wg.Wait()永远等不到三个wg.Done(),也陷入阻塞。最终所有goroutine都休眠,触发死锁。
优化后的代码
要确保所有subscribe goroutine都进入Wait状态后,再执行Broadcast。可以用额外的WaitGroup来同步准备状态:
package main import ( "fmt" "sync" ) type Button struct { Clicked *sync.Cond } func subscribe(cond *sync.Cond, btnMessage string, wg *sync.WaitGroup, readyWG *sync.WaitGroup) { defer wg.Done() cond.L.Lock() // 告诉main:我已经加锁,准备好等待了 readyWG.Done() cond.Wait() fmt.Println(btnMessage) cond.L.Unlock() } func test6() { var wg sync.WaitGroup var readyWG sync.WaitGroup button := Button{ Clicked: sync.NewCond(&sync.Mutex{}), } wg.Add(3) readyWG.Add(3) go subscribe(button.Clicked, "Button 1", &wg, &readyWG) go subscribe(button.Clicked, "Button 2", &wg, &readyWG) go subscribe(button.Clicked, "Button 3", &wg, &readyWG) // 等待所有goroutine都准备好进入Wait状态 readyWG.Wait() button.Clicked.Broadcast() wg.Wait() } func main() { test6() }
优化点说明:
- 新增
readyWG用来同步subscribe的准备状态,每个goroutine加锁后先调用readyWG.Done(),告诉main自己已经准备好等待。 - main先等待
readyWG.Wait(),确认三个goroutine都进入等待状态后,再执行Broadcast,此时通知能被所有goroutine接收到。
死锁定位方法
分析Go自动输出的死栈信息
Go运行时会自动检测全局死锁,打印所有goroutine的调用栈:- 从日志里能看到goroutine 1卡在
sync.(*WaitGroup).Wait,说明main在等子goroutine结束; - goroutine 6-8卡在
sync.(*Cond).Wait,说明这些子goroutine一直在等待条件通知。结合这两点就能推断出通知没被接收到,导致双向阻塞。
- 从日志里能看到goroutine 1卡在
添加关键日志调试
在加锁后、Wait前、Broadcast前后打印日志,观察执行顺序:// 在subscribe里加日志 cond.L.Lock() fmt.Printf("%s: 已加锁,准备Wait\n", btnMessage) readyWG.Done() cond.Wait() // 在main里加日志 readyWG.Wait() fmt.Println("所有goroutine准备完毕,执行Broadcast") button.Clicked.Broadcast()通过日志能直观看到Broadcast是否在所有Wait之前执行,快速定位时机问题。
使用pprof分析goroutine状态
运行程序时开启pprof:go run -cpuprofile cpu.pprof main.go,然后用go tool pprof cpu.pprof进入交互模式,输入goroutine命令查看所有goroutine的阻塞状态,精准定位阻塞点。
内容的提问来源于stack exchange,提问作者cool_fire
相关产品推荐
相关产品推荐

