Goroutines死锁问题:递归DFS函数并发执行的困境
解决方案:修复DFS并发的死锁问题
核心问题分析
你遇到的死锁是因为每个通过go dfs(...)启动的goroutine都对应一次wg.Add(1),但没有匹配的wg.Done()。直接在递归的dfs函数内调用wg.Done()会导致多次调用(每个递归分支都会执行),远超Add的次数,同样会引发错误。正确的做法是让每个顶层goroutine(即循环中启动的那一批)在自身负责的DFS分支完全执行完毕后,再调用一次wg.Done()。
具体修复方案
1. 用包装函数+defer确保Done调用
修改goroutine的启动逻辑,用匿名函数包装DFS调用,并通过defer wg.Done()保证无论DFS如何结束(正常返回或异常),都会触发Done操作。同时解决两个隐藏问题:循环变量复用的陷阱,以及共享变量max_path的并发安全问题。
修复后的完整代码示例:
import "sync" var ( maxPath []int mu sync.Mutex // 保护maxPath的互斥锁 ) func dfs(data map[int][]int, path []int) { datum := path[len(path)-1] value := data[datum] for _, v := range value { if !contains(path, v) { // 复制原路径,避免底层数组共享导致的路径混乱 newPath := make([]int, len(path)) copy(newPath, path) newPath = append(newPath, v) // 更新最长路径时加锁,避免并发写入竞态 mu.Lock() if len(newPath) > len(maxPath) { maxPath = newPath } mu.Unlock() dfs(data, newPath) } } } // 启动并发DFS的代码 func startConcurrentDFS(graph map[int][]int, nodes []int, wg *sync.WaitGroup) { for _, node := range nodes { if is_touching_wall(node) { wg.Add(1) // 传入循环变量的副本,避免goroutine复用同一变量值 go func(startNode int) { defer wg.Done() // 确保DFS分支结束后调用Done dfs(graph, []int{startNode}) }(node) } } }
2. 关键注意点
- 循环变量副本:Go的for循环变量是复用的,直接在goroutine中引用
node会导致所有goroutine可能拿到同一个最终值,必须通过匿名函数参数传入副本。 - 共享变量加锁:
maxPath是多个goroutine共享的变量,并发写入会导致数据竞争(即使你只做长度比较和赋值),必须用互斥锁保护。 - 路径复制:原代码中
append(path, ...)可能复用原path的底层数组,导致不同递归分支的路径被意外修改,显式复制路径可以避免这个问题。
替代思路:用通道控制并发
如果不想使用WaitGroup,也可以用通道来收集所有顶层DFS分支的结束信号:
func startConcurrentDFS(graph map[int][]int, nodes []int) { doneChan := make(chan struct{}) count := 0 for _, node := range nodes { if is_touching_wall(node) { count++ go func(startNode int) { dfs(graph, []int{startNode}) doneChan <- struct{}{} }(node) } } // 等待所有分支结束 for i := 0; i < count; i++ { <-doneChan } close(doneChan) }
这种方式不需要WaitGroup,逻辑更直观,但本质和WaitGroup是一样的,都是跟踪顶层goroutine的生命周期。
内容的提问来源于stack exchange,提问作者ahmetnecdeto
相关产品推荐
相关产品推荐

