You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.23 08:45:28