What is an elegant way to shut down a chain of goroutines linked by channels?

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

What is an elegant way to shut down a chain of goroutines linked by channels?

问题

我是一个Go语言学习者。为了更好地理解通道和goroutine的使用,我正在尝试构建一个埃拉托斯特尼筛法(Sieve of Eratosthenes),将其作为一组通过通道连接的goroutine管道。

以下是我目前的代码:

// esieve 实现了埃拉托斯特尼筛法
// 作为一系列通过通道连接的goroutine
package main

import "fmt"

func sieve(mine int, inch chan int) {
    start := true                        // 第一个数字的开关
    ouch := make(chan int)               // 该实例的输出通道
    fmt.Printf("%v\n", mine)             // 打印该实例的质数
    for next := <-inch; next > 0; next = <-inch {  // 读取输入通道
        fmt.Printf("%v <- %v\n",mine,next)         // (跟踪)
        if (next % mine) > 0 {                     // 能被我的质数整除吗?
            if start {                   // 不行;它是第一个数字吗?
                go sieve(next, ouch)     // 第一个数字 - 为其创建实例
                start = false            // 第一次完成
            } else {                     // 不是第一次
                ouch <- next             // 将其传递给下一个实例
            }
        }
    }
}

func main() {
    lim := 30                     // 让我们处理到30
    fmt.Printf("%v\n", 2)         // 将2视为特殊情况
    ouch := make(chan int)        // 创建管道的第一个段
    go sieve(3, ouch)             // 为'3'创建实例
    for prime := 3; prime < lim; prime += 2 { // 生成3, 5, ...
        fmt.Printf("Send %v\n", prime)	      // 跟踪
        ouch <- prime                         // 将其发送到管道中
    }
}

就目前而言,它工作得很好。

然而,当我完成主循环后,main函数会在所有仍在sieve实例的管道中传播的数字到达末尾之前退出。

有什么最简单、最优雅或通常被接受的方法可以使主程序等待一组goroutine(它只“知道”第一个goroutine)完成?

英文:

I'm a Go learner. In order better to understand the care and feeding of channels and goroutines, I'm trying to build a Sieve of Eratosthenes as a set of goroutines connected into a pipeline by channels.

Here's what I have so far:

// esieve implements a Sieve of Eratosthenes
// as a series of channels connected together
// by goroutines
package main

import &quot;fmt&quot;

func sieve(mine int, inch chan int) {
	start := true                        // First-number switch
	ouch := make(chan int)               // Output channel for this instance
	fmt.Printf(&quot;%v\n&quot;, mine)             // Print this instance&#39;s prime
	for next := &lt;-inch; next &gt; 0; next = &lt;-inch {  // Read input channel
		fmt.Printf(&quot;%v &lt;- %v\n&quot;,mine,next)         // (Trace)
		if (next % mine) &gt; 0 {                     // Divisible by my prime?
			if start {                   // No; is it the first number through? 
				go sieve(next, ouch)     // First number - create instance for it
				start = false            // First time done
			} else {                     // Not first time
				ouch &lt;- next             // Pass it to the next instance
			}
		}
	}
}

func main() {
	lim := 30                     // Let&#39;s do up to 30
	fmt.Printf(&quot;%v\n&quot;, 2)         // Treat 2 as a special case
	ouch := make(chan int)        // Create the first segment of the pipe
	go sieve(3, ouch)             // Create the instance for &#39;3&#39;
	for prime := 3; prime &lt; lim; prime += 2 { // Generate 3, 5, ...
		fmt.Printf(&quot;Send %v\n&quot;, prime)	      // Trace
		ouch &lt;- prime                         // Send it down the pipe
	}
}

And as far as it goes, it works nicely.

However, when I finish the main loop, main exits before all the numbers still in the pipeline of sieve instances have propagated down to the end.

What is the simplest, most elegant, or generally accepted way to make a main routine wait for a set of goroutines (about which it only 'knows' of the first one) to complete?

答案1

得分: 2

关于你的标题问题,当你不再需要它们时如何终止工作协程:

你可以使用"Done"惯用法。从一个关闭的通道读取会产生零值。

创建一个新的通道done。当从这个通道成功读取时,协程知道它们应该退出。在主函数中当你获得所有需要的值时关闭这个通道。

检查是否可以从通道done读取,并通过返回退出,或者当下一个值可用时从通道中读取。这部分替代了你的for循环中对next的赋值:

select {
case <-done:
    return
case next = <-inch:
}

也可以使用通道的范围遍历,因为关闭通道会退出循环。

关于你的具体问题,等待一组协程完成:

使用sync.WaitGroup

var wg sync.WaitGroup
wg.Add(goroutineCount)

当每个协程完成时:

wg.Done()

或者使用defer:

defer wg.Done()

等待它们全部报告完成:

wg.Wait()

在你的示例中,当你启动一个新的协程时,只需调用wg.Add(1),然后在调用wg.Done()并返回之前。只要只达到零一次,wg.Wait()就会按预期工作,所以在wg.Done()之前调用wg.Add(1)

英文:

As for your title question, killing worker goroutines when you don't need them anymore:
You could use the Done idiom. Reads from a closed channel yield the zero value.

Make a new channel done. When reads from this channel succeed, the goroutines know they should quit. Close the channel in main when you have all the values you need.

Check if you can read from a channel done, and exit by returning, or read from next when that's available. This partially replaces the assignment to next in you for loop:

select {
case &lt;-done:
return
case next = &lt;- inch:
}

Ranging over a channel also works, since closing that channel exits the loop.

As for the reverse, your body question, waiting for a set of goroutines to finish:

Use sync.WaitGroup.

var wg sync.WaitGroup
wg.Add(goroutineCount)

And when each goroutine finishes:

wg.Done()

Or use defer:

defer wg.Done()

To wait for all of them to report as Done:

wg.Wait()

In your example, simply call wg.Add(1) when you start a new goroutine, before you call wg.Done() and return. As long as you only reach zero once, wg.Wait() works as expected, so wg.Add(1) before wg.Done.

答案2

得分: 0

@izca解除我的僵局之后,经过几次失败的尝试,当一切都完成时发生了死锁,这是我的正确解决方案:

// esieve实现了埃拉托斯特尼筛法
// 作为一系列通过goroutine连接在一起的通道
package main

import "fmt"

func sieve(mine int,                  // 该实例自己的质数
           inch chan int,             // 从较低质数输入的通道
           done chan int,             // 用于发出关闭信号的通道
           count int) {               // 质数的数量 - 计数器
    start := true                     // 第一个数字开关
    ouch := make(chan int)            // 输出通道,该实例
    fmt.Printf("%v ", mine)           // 打印该实例的质数
    for next := <-inch; next > 0; next = <-inch { // 读取输入通道
        if (next % mine) > 0 {        // 能被我的质数整除吗?
            if start {                // 不行;第一次通过吗?
                go sieve(next, ouch, done, count+1) // 第一个数字,
                                                    // 为其创建实例
                start = false         // 第一次完成
            } else {                  // 不是第一次
                ouch <- next          // 传递给下一个实例
            }
        }
    }
    if start {                        // 刚开始?
        close(done)                   // 是的 - 我们是管道中的最后一个 - 发出完成信号
        print("\n",count," primes\n") // 质数/ goroutine的数量
    } else {
        close(ouch)                   // 不是 - 将信号发送到管道下方
    }
}

func main() {
    lim := 100                        // 让我们做到100
    done := make(chan int)            // 创建完成返回通道
    ouch := make(chan int)            // 创建管道的第一段
    go sieve(2, ouch, done, 1)        // 为'2'创建第一个实例
    for prime := 3; prime < lim; prime += 1 { // 生成奇数
        ouch <- prime                         // 将数字发送到管道中
    }
    close(ouch)                       // 将完成信号发送到管道中
    <- done                           // 并等待它返回
}

与许多其他语言相比,我对Go在这种编程方面的优雅和简洁印象深刻。当然,我也有自己的缺点。

如果适用的话,我欢迎批评性的评论。

英文:

After @izca unblocked my logjam, and after a few false starts involving deadlocks when everything finished, here's my solution working correctly:

// esieve implements a Sieve of Eratosthenes
// as a series of channels connected together
// by goroutines
package main

import &quot;fmt&quot;

func sieve(mine int,                  // This instance&#39;s own prime
	       inch chan int,             // Input channel from lower primes
	       done chan int,             // Channel for signalling shutdown
	       count int) {               // Number of primes - counter
	start := true                     // First-number switch
	ouch := make(chan int)            // Output channel, this instance
	fmt.Printf(&quot;%v &quot;, mine)           // Print this instance&#39;s prime
	for next := &lt;-inch; next &gt; 0; next = &lt;-inch { // Read input channel
		if (next % mine) &gt; 0 {        // Divisible by my prime?
			if start {                // No; first time through?
				go sieve(next, ouch, done, count+1) // First number,
				                                    // create instance for it
				start = false         // First time done
			} else {                  // Not first time
				ouch &lt;- next          // Pass to next instance
			}
		}
	}
	if start {                        // Just starting?
		close(done)                   // Yes - we&#39;re last in pipe - signal done
		print(&quot;\n&quot;,count,&quot; primes\n&quot;) // Number of primes/goroutines
	} else {
		close(ouch)                   // No - send the signal down the pipe
	}
}

func main() {
	lim := 100                        // Let&#39;s do up to 100
	done := make(chan int)            // Create the done return channel
	ouch := make(chan int)            // Create the first segment of the pipe
	go sieve(2, ouch, done, 1)        // Create the first instance for &#39;2&#39;
	for prime := 3; prime &lt; lim; prime += 1 { // Generate odd numbers
		ouch &lt;- prime                         // Send numbers down the pipe
	}
	close(ouch)                       // Send the done signal down the pipe
	&lt;- done                           // and wait for it to come back
}

I'm tremendously impressed with the elegance and simplicity of Go for this kind of programming, when compared with many other languages. Of course, the warts I claim for myself.

If appropriate here, I'd welcome critical comments.

huangapple
  • 本文由 发表于 2016年1月17日 03:51:54
  • 转载请务必保留本文链接:https://go.coder-hub.com/34831396.html
匿名

发表评论

匿名网友

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

确定