如何判断一个 goroutine 是否成功执行或者所有的 goroutine 都已完成?

huangapple go评论102阅读模式
英文:

How to tell if one goroutine succeeded or all goroutines are done?

问题

我正在尝试使用深度优先搜索(DFS)来检查图中的循环,通过检查图中的每个节点来判断是否存在循环。

我想为每个节点运行一个goroutine,并在检测到第一个循环或没有循环时终止。

当检测到第一个循环时终止似乎没问题,但是我在将其与没有更多节点时终止混合时遇到了问题。

下面是一个看起来似乎可以工作的实现,但我觉得它看起来有点笨拙,我正在寻找更好的实现方式。我基本上使用带缓冲的通道,并在每次没有检测到循环时增加一个计数器,当计数器达到底层图的大小时终止。

对我来说,使用计数器来确定何时完成似乎不太符合惯用方式。或者,更准确地说,如果在Go中有更符合惯用方式的方法,那将是很好的。

func (c *cycleDet) hasCycle() bool {
	adj := c.b // 底层图的邻接表表示
	hasCycles := make(chan bool, len(adj))

	for node := range adj {
		go func(no nodeID, co chan<- bool) {
			visited := make(map[nodeID]struct{})
			// c.nodeHasCycle将使用DFS的递归实现来查找节点no是否导致循环。
			if c.nodeHasCycle(no, visited) {
				co <- true
				return
			}
			co <- false
		}(node, hasCycles)
	}

	var i int
	for {
		select {
		case v := <-hasCycles:
			if v {
				fmt.Println("检测到循环")
				return true
			}
			if i == len(c.b) {
				return false
			}
			i++
		}
	}
}

我还遇到了另一个推荐使用"观察者"goroutine的SO帖子(尽管我找不到该SO帖子)。我不确定它的具体实现,但是想象它可能与WaitGroup混合使用,如下所示:

func (c *cycleDet) hasCycle() bool {
	hasCycle := make(chan bool)
	done := make(chan struct{})
	var wg sync.WaitGroup

    adj := c.b // 底层图的邻接表表示
	for node := range adj {
		go func(no nodeID, co chan<- bool) {
			// 使用wg来同步只读goroutine的终止。
			wg.Add(1)
			defer wg.Done()

			visited := make(map[nodeID]struct{})
			// c.nodeHasCycle将使用DFS的递归实现来查找节点no是否导致循环。
			if c.nodeHasCycle(no, visited) {
				co <- true
				return
			}
		}(node, hasCycle)
	}

	// 观察者goroutine在wg等待完成时发出通知。
	time.Sleep(100 * time.Millisecond)
	go func() {
		wg.Wait()
		done <- struct{}{}
	}()

	select {
	case <-hasCycle:
		fmt.Println("检测到循环")
		return true
	case <-done:
		fmt.Println("未检测到循环")
		return false
	}
}

然而,这种方法让我担心,因为我需要在第一个WaitGroup计数器增加后才开始等待观察者,这似乎比第一个解决方案更加脆弱。

英文:

I am trying to use DFS to check a graph for cycles by checking each node in the graph for a cycle.

I would like to run a goroutine for each node and terminate when the first cycle is detected or when there are no cycles.

Terminating when the first cycle is detected seems fine, but I am having trouble mixing that with when there aren't anymore nodes.

Below is an implementation that seems to work, but looks janky to me and I was looking for a better implementation. I essentially use buffered channels and increment a counter every time I don't get a cycle and terminate when the counter reaches the size of the underlying graph.

Using the counter to determine when we are done seems un-idiomatic to me. Or, rather, it'd be great to know if there is a more idiomatic way to do this in Go.

func (c *cycleDet) hasCycle() bool {
	adj := c.b // adjacency list representation of the unerlying graph
	hasCycles := make(chan bool, len(adj))

	for node := range adj {
		go func(no nodeID, co chan&lt;- bool) {
			visited := make(map[nodeID]struct{})
			// c.nodeHasCycle will use a recursive implementation of DFS to
			// find out if the node no leads to a cycle.
			if c.nodeHasCycle(no, visited) {
				co &lt;- true
				return
			}
			co &lt;- false
		}(node, hasCycles)
	}

	var i int
	for {
		select {
		case v := &lt;-hasCycles:
			if v {
				fmt.Println(&quot;got cycle&quot;)
				return true
			}
			if i == len(c.b) {
				return false
			}
			i++
		}
	}
}

I've also come across another SO post that recommended using an "observer" goroutine (although I can't find the SO post). I'm not sure what that would look like but imagine it would mix with WaitGroups like this:

func (c *cycleDet) hasCycle() bool {
	hasCycle := make(chan bool)
	done := make(chan struct{})
	var wg sync.WaitGroup

    adj := c.b // adjacency list representation of the unerlying graph
	for node := range adj {
		go func(no nodeID, co chan&lt;- bool) {
			// Use wg to synchronize termination of read-only goroutines.
			wg.Add(1)
			defer wg.Done()

			visited := make(map[nodeID]struct{})
			// c.nodeHasCycle will use a recursive implementation of DFS to
			// find out if the node no leads to a cycle.
			if c.nodeHasCycle(no, visited) {
				co &lt;- true
				return
			}
		}(node, hasCycle)
	}

	// Observer goroutine to notify when wg is done waiting.
	time.Sleep(100 * time.Millisecond)
	go func() {
		wg.Wait()
		done &lt;- struct{}{}
	}()

	select {
	case &lt;-hasCycle:
		fmt.Println(&quot;got a cycle&quot;)
		return true
	case &lt;-done:
		fmt.Println(&quot;no cycle detected&quot;)
		return false
	}
}

However this approach worries me because I need to sleep for the observer to start waiting only after the first WaitGroup counter increments, which seems far more fragile than the first solution.

答案1

得分: 1

你在使用WaitGroup方面是正确的,但是你没有正确使用它。首先,你需要在调用wg.Done()的go例程之外调用wg.Add(1)。其次,调用wg.Wait()会阻塞,直到等待组中的所有go例程执行完毕,所以你不希望它并发执行。

至于短路,实际上没有一个很好的方法来实现。我的建议是使用上下文(context)。在这种情况下,你需要将上下文传递给nodeHasCycle的调用,如果你想要真正的短路行为。

修复你的代码,我们有:

func (c *cycleDet) hasCycle() bool {
    ctx, cancel := context.WithCancel(context.Background())
    hasCycle := make(chan bool)
    var wg sync.WaitGroup

    adj := c.b // underlying graph's adjacency list representation
    for node := range adj {
        wg.Add(1)
        go func(ctx context.Context, no nodeID, co chan<- bool, cancel context.CancelFunc) {
            // Use wg to synchronize termination of read-only goroutines.
            defer wg.Done()

            select {
                case <-ctx.Done():
                    return
                default:
            }

            visited := make(map[nodeID]struct{})
            // c.nodeHasCycle will use a recursive implementation of DFS to
            // find out if the node no leads to a cycle.
            if c.nodeHasCycle(ctx, no, visited) {
                co <- true
                cancel()
                return
            }
        }(ctx, node, hasCycle, cancel)
    }

    // Observer goroutine to notify when wg is done waiting.
    time.Sleep(100 * time.Millisecond)
    wg.Wait()
    defer cancel()

    select {
    case <-hasCycle:
        fmt.Println("got a cycle")
        return true
    default:
        fmt.Println("no cycle detected")
        return false
    }
}

通过这种设置,你可以确保除非找到一个循环,否则所有调用go例程的操作都会执行。在找到循环的情况下,不会调用额外的go例程,并且如果你在nodeHasCycle中添加了检查取消的逻辑,那么你也可以停止正在运行的调用。

英文:

You are correct in that WaitGroup is probably what you want. However, you're not using it correctly. First, you need to call wg.Add(1) outside of the go routine that is calling wg.Done(). Second, calling wg.Wait() blocks until all the go routines in the wait group finish execution, so you don't want it to execute concurrently.

As for short-circuiting, there's not really a good way to do that. My advice would be to use a context. One thing you'll have to do in this case is to wire the context into your calls of nodeHasCycle if you want real short-circuiting behavior.

Fixing you code, we have:

func (c *cycleDet) hasCycle() bool {
    ctx, cancel := context.WithCancel(context.Background())
    hasCycle := make(chan bool)
    var wg sync.WaitGroup

    adj := c.b // adjacency list representation of the unerlying graph
    for node := range adj {
        wg.Add(1)
        go func(ctx context.Context, no nodeID, co chan&lt;- bool, cancel context.CancelFunc) {
            // Use wg to synchronize termination of read-only goroutines.
            defer wg.Done()

            select {
                case &lt;-ctx.Done():
                    return
                default:
            }

            visited := make(map[nodeID]struct{})
            // c.nodeHasCycle will use a recursive implementation of DFS to
            // find out if the node no leads to a cycle.
            if c.nodeHasCycle(ctx, no, visited) {
                co &lt;- true
                cancel()
                return
            }
        }(ctx, node, hasCycle, cancel)
    }

    // Observer goroutine to notify when wg is done waiting.
    time.Sleep(100 * time.Millisecond)
    wg.Wait()
    defer cancel()

    select {
    case &lt;-hasCycle:
        fmt.Println(&quot;got a cycle&quot;)
        return true
    default:
        fmt.Println(&quot;no cycle detected&quot;)
        return false
    }
}

With it setup this way, you can ensure that all your calls to the go-routine will operate unless a cycle is found, in which case no additional go-routes will be called and, if you add logic to check for cancellation into nodeHasSycle, then you can stop execution on any running calls to it as well.

huangapple
  • 本文由 发表于 2022年5月24日 07:55:21
  • 转载请务必保留本文链接:https://go.coder-hub.com/72355896.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定