Go语言并发deadlock问题排查与正确实现方案咨询
Go语言并发deadlock问题排查与正确实现方案咨询
嘿,我来帮你理清楚这个死锁问题到底是怎么回事,以及该怎么解决~
首先咱们先拆解一下你代码里的死锁形成过程,这样你就能明白为什么会报错了:
- 你创建了一个缓冲大小为2的
signalchannel,然后启动了一个worker goroutine,它会一直循环从signal里接收值。 - 假设main里的
scan函数第一次往signal发送了true,此时channel里有1个元素,worker收到这个值后,同步调用了doWork。 - 在
doWork里,你又执行了signal <- true,这时候channel的缓冲被填满了(2个元素),然后程序进入2秒的睡眠。 - 等睡眠结束后,worker的循环会接着接收刚才
doWork发送的第二个true,然后又一次同步调用doWork。 - 这时候
doWork里再执行signal <- true,发现channel缓冲已经满了,这个发送操作就会阻塞——而worker现在卡在doWork的发送步骤里,根本没法回到for循环去接收channel里的值,自然也没人能取走channel里的元素来腾出空间。 - 同时main goroutine在无限循环调用
scan,如果scan也是往signal发值的话,此时也会因为缓冲满而阻塞。到这一步,所有goroutine都卡住了,Go runtime检测到没有任何goroutine能继续推进程序,就抛出了死锁错误。
那该怎么修改才能正确实现你想要的并发逻辑呢?这里给你两个可行的方案:
方案一:用goroutine异步执行doWork,避免阻塞worker循环
把同步调用doWork改成启动新的goroutine去执行,这样worker的接收循环不会被阻塞,能一直处理channel里的信号,同时doWork的执行也不会影响channel的收发:
package main import ( "fmt" "log/slog" "time" ) func main() { signal := make(chan bool, 2) go func() { slog.Default().Info("worker started") for s := range signal { if s { // 启动新goroutine执行doWork,不阻塞当前接收循环 go doWork() } } slog.Default().Info("worker stopped") }() // 模拟scan的触发逻辑,比如每隔1秒发一次信号 for { signal <- true time.Sleep(1 * time.Second) } } func doWork() { fmt.Println("work started") time.Sleep(2 * time.Second) fmt.Println("work finished") // 如果需要重复执行doWork,直接在这里启动新的goroutine即可 // go doWork() }
方案二:调整触发逻辑,不依赖原channel自循环
如果你的需求是doWork执行完成后需要再次执行,完全不用通过往signal发信号的方式触发,直接在doWork里判断条件后启动新goroutine就行,这样能彻底避免channel阻塞的问题:
func doWork() { fmt.Println("work started") time.Sleep(2 * time.Second) fmt.Println("work finished") // 这里可以根据业务逻辑判断是否需要再次执行 needRetry := true if needRetry { go doWork() } }
总结一下,你原来的问题核心在于同步调用doWork并往同一个缓冲channel里发信号,导致channel满了之后发送阻塞,进而让所有goroutine都卡停。改成异步执行的方式,就能让各个goroutine独立运行,不会互相阻塞啦。
备注:内容来源于stack exchange,提问作者Jed
相关产品推荐
相关产品推荐

