Go中Cond场景下fmt.Println置于wg.Done后引发死锁的原因咨询
Go语言发布订阅模式下的偶发死锁原因分析
问题场景
以下是一段采用发布订阅模式的Go代码,当把fmt.Println("waiting")放在goroutineRunning.Done()之后时,会偶发死锁;移除该语句或把它移到Done()之前则运行正常,死锁需要多次执行go run main.go才会触发:
package main import ( "fmt" "sync" ) func main() { cond := sync.NewCond(&sync.Mutex{}) subscribe := func(c *sync.Cond, fn func()) { var goroutineRunning sync.WaitGroup goroutineRunning.Add(1) go func() { goroutineRunning.Done() fmt.Println("waiting") c.L.Lock() c.Wait() c.L.Unlock() fn() }() goroutineRunning.Wait() } var clickRegistered sync.WaitGroup clickRegistered.Add(2) subscribe(cond, func() { fmt.Println("notified 1") clickRegistered.Done() }) subscribe(cond, func() { fmt.Println("notified 2") clickRegistered.Done() }) cond.Broadcast() clickRegistered.Wait() }
死锁原因解析
核心问题在于条件变量的广播时机和goroutine进入等待状态的时机不匹配,具体过程如下:
- 当
goroutineRunning.Done()先执行时,主goroutine里的goroutineRunning.Wait()会立刻返回,继续执行后续代码(比如第二个subscribe,或者直接走到cond.Broadcast())。 - 订阅goroutine在调用
Done()后,还要执行fmt.Println("waiting")——这个IO操作会消耗少量时间,同时Go调度器可能在这个节点切换回主goroutine。 - 如果主goroutine在某个订阅goroutine还没执行到
c.Wait()的时候,就调用了cond.Broadcast(),那么这个订阅goroutine后续再调用c.Wait()时会永远阻塞:因为条件变量的广播是一次性的,错过这次广播后,没有其他通知能唤醒它。 - 被阻塞的订阅goroutine永远不会执行
fn(),也就不会调用clickRegistered.Done(),主goroutine的clickRegistered.Wait()会一直等待,最终导致整个程序死锁。
而把fmt.Println("waiting")移到goroutineRunning.Done()之前,或者移除它时,订阅goroutine会更快地进入c.Wait()状态,主goroutine的goroutineRunning.Wait()返回时,订阅goroutine大概率已经处于等待状态,此时cond.Broadcast()能正确唤醒所有等待的goroutine,不会出现死锁。
内容的提问来源于stack exchange,提问作者Hari
相关产品推荐
相关产品推荐

