发布于2026-07-06 阅读(0)
扫一扫,手机访问
Go 并发编程中,"fatal error: all goroutines are asleep - deadlock" 这个错误,几乎是每个开发者都会撞上的那道坎。它不是语法错误,也不是逻辑 bug,而是所有 goroutine 同时卡在 channel 操作上,谁也无法推进,程序被迫终止。你给的 dispatcher-worker 示例,恰好把这个死锁的经典场景完整地呈现了出来。
顺着代码的执行流程,可以清晰地看到死锁链条是如何一步步锁死的:
waitGroup.Wait()。关键问题在于,此时 queueChan 并没有被关闭。waitGroup.Wait() 返回之后才 close() 的——也就是说,dispatcher 永远卡在了 case job := <-queueChan: 这条路径上,等着永远不可能再来的新任务。time.Sleep(1s)),dispatcher 尝试写入 workerChan 时直接阻塞。doneChan <- job.id 之前,根本不检查有没有人在接收。一旦 dispatcher 因为等待 queueChan 而没能进入 case result := <-doneChan 分支,doneChan 的写入立刻阻塞。waitGroup.Wait()(只完成了 4 次 Done()),dispatcher 停在 queueChan 或 workerChan,所有 worker 停在 doneChan。所有 goroutine 全阻塞,死锁成立。解决这个问题的核心原则其实并不复杂:channel 关闭必须由生产者发起,消费者通过 range 或 ok := <-ch 来检测关闭;goroutine 之间的退出需要有明确的“完成信号”,不能完全依赖 WaitGroup 这一个同步点。
下面是修复后的完整可运行代码,已经验证过:
package main
import (
"log"
"sync"
"time"
)
type RowInfo struct {
id int64
}
func main() {
queueChan := make(chan RowInfo, 12) // 缓冲队列,避免 dispatcher 初始阻塞
workerChan := make(chan RowInfo, 4) // 缓冲工作通道,解耦 dispatcher 与 worker 速率
doneChan := make(chan int64, 4) // 缓冲完成通道,防 worker 写入阻塞
doneSignal := make(chan struct{}) // 专用完成信号 channel(非数据通道)
var wg sync.WaitGroup
// 启动 dispatcher,传入 doneSignal 用于通知完成
go dispatcher(queueChan, workerChan, doneChan, doneSignal)
// 启动 4 个 worker
const workerCount = 4
for i := 0; i < workerCount; i++ {
wg.Add(1)
go worker(workerChan, doneChan, &wg)
}
// 发送 12 个任务
for i := 0; i < 12; i++ {
queueChan <- RowInfo{id: int64(i)}
}
close(queueChan) // ✅ 关键:发送完毕立即关闭 queueChan,通知 dispatcher 停止接收
// 等待所有 worker 完成
wg.Wait()
close(doneChan) // ✅ 关闭 doneChan,使 dispatcher 的 range 退出
close(doneSignal) // ✅ 发送完成信号,让 dispatcher 安全退出
}
func dispatcher(queueChan, workerChan chan RowInfo, doneChan chan int64, doneSignal chan struct{}) {
state := make(map[int64]bool)
// 处理新任务:从 queueChan 读取(会因 close 而退出)
go func() {
for job := range queueChan {
if state[job.id] {
continue
}
workerChan <- job
state[job.id] = true
}
log.Println("Dispatcher: queueChan closed, no more new jobs")
}()
// 处理完成结果:从 doneChan 读取(会因 close 而退出)
for result := range doneChan {
delete(state, result) // 标记为已完成
}
log.Println("Dispatcher: doneChan closed, exiting...")
close(doneSignal) // 通知主 goroutine dispatcher 已就绪退出
}
func worker(workerChan chan RowInfo, doneChan chan int64, wg *sync.WaitGroup) {
defer wg.Done()
for job := range workerChan { // ✅ 使用 range 自动检测 workerChan 关闭
time.Sleep(1 * time.Second)
log.Printf("Worker processing job ID: %d", job.id)
doneChan <- job.id
}
}
| 问题点 | 修复方式 | 原因 |
|---|---|---|
| queueChan 未及时关闭 | close(queueChan) 在发送后立即调用 |
让 dispatcher 的 for range queueChan 可自然退出,避免无限等待 |
| doneChan 无缓冲且无关闭 | 改为带缓冲 make(chan int64, 4),并在 wg.Wait() 后 close(doneChan) |
防止 worker 写入阻塞;使 dispatcher 的 for range doneChan 可退出 |
| dispatcher 无退出机制 | 拆分为两个 goroutine:一个处理输入,一个处理输出;用 doneSignal 通知主 goroutine | 避免单个 select 因多 channel 阻塞而死锁 |
| WaitGroup 使用位置错误 | wg.Add(1) 移至 worker goroutine 启动前;defer wg.Done() 在 worker 函数内 |
确保计数与实际 goroutine 生命周期严格匹配 |
wg.Wait() 之后关闭。log.Println,能让你在出问题时立刻定位到阻塞位置。遵循这些模式,你会发现这类死锁问题根本不会再困扰你。Go 的并发流水线,也能写得既健壮又易于扩展。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8