优雅停止监听频道的更好方法

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

Better way to gracefully stop listening to channels

问题

我是Go的学习者。为了在Go中模拟并发,我编写了下面的示例代码:

package main

import (
	"fmt"
	"time"
)

func main() {
	c1 := make(chan string)
	c2 := make(chan string)
	go func() {
		for i := 0; i < 5; i++ {
			time.Sleep(500 * time.Millisecond)
			c1 <- "每500毫秒"
		}
		close(c1)
	}()
	go func() {
		for i := 0; i < 5; i++ {
			time.Sleep(1000 * time.Millisecond)
			c2 <- "每1秒"
		}
		close(c2)
	}()
	for {
		select {
		case msg1, ok := <-c1:
			if !ok {
				return
			}
			fmt.Println(msg1)
		case msg2, ok := <-c2:
			if !ok {
				return
			}
			fmt.Println(msg2)
		}
	}
}

如你所见,在main函数的无限循环中,我通过使用变量isDone手动退出循环。但是,我觉得这种方法很繁琐。难道不可以在select条件中使用range遍历通道吗?

以上代码的输出为(Go Playground链接):

每500毫秒
每1秒
每500毫秒
每500毫秒
每1秒
每500毫秒
每500毫秒
英文:

I am a learner of Go. In order to simulate concurrency in Go, I wrote the following example below:

package main

import (
	&quot;fmt&quot;
	&quot;time&quot;
)

func main() {
	c1 := make(chan string)
	c2 := make(chan string)
	go func() {
		for i := 0; i &lt; 5; i++ {
			time.Sleep(500 * time.Millisecond)
			c1 &lt;- &quot;Every 500 ms&quot;
		}
		close(c1)
	}()
	go func() {
		for i := 0; i &lt; 5; i++ {
			time.Sleep(1000 * time.Millisecond)
			c2 &lt;- &quot;Every 1 s&quot;
		}
		close(c2)
	}()
	isDone := false
	for !isDone {
		select {
		case msg1, ok := &lt;-c1:
			if !ok {
				isDone = true
				break
			}
			{
				fmt.Println(msg1)
			}
		case msg2, ok := &lt;-c2:
			if !ok {
				isDone = true
				break
			}
			{
				fmt.Println(msg2)
			}
		}
	}
}

As you can see, inside the infinite for loop in the main function, I am manually getting out of the loop by using the variable isDone. But this approach looks cumbersome to me. Isn't it possible to do range over channel along with the select condition?

The output of the above code is (Go Playground Link):

Every 500 ms
Every 1 s
Every 500 ms
Every 500 ms
Every 1 s
Every 500 ms
Every 500 ms

答案1

得分: 1

问题在于:

> "break" 语句会终止同一函数内最内层的 "for"、"switch" 或 "select" 语句的执行。

因此,我们无法在退出 "select" 语句后直接退出 "for" 循环。我稍微修改了你的代码,以简化实现:

package main

import (
	"fmt"
	"time"
)

func main() {
	c1 := make(chan string)
	c2 := make(chan string)
	go func() {
		for i := 0; i < 5; i++ {
			time.Sleep(500 * time.Millisecond)
			c1 <- "Every 500 ms"
		}
		close(c1)
	}()
	go func() {
		for i := 0; i < 5; i++ {
			time.Sleep(1000 * time.Millisecond)
			c2 <- "Every 1 s"
		}
		close(c2)
	}()

	ok := true
	var msg1 string
	var msg2 string

	for {
		select {
		case msg1, ok = <-c1:
			if !ok {
				break
			}
			{
				fmt.Println(msg1)
			}
		case msg2, ok = <-c2:
			if !ok {
				break
			}
			{
				fmt.Println(msg2)
			}
		}

		if !ok {
			break
		}
	}

	fmt.Println("For loop exited")
}

以上代码的 Go Playground 链接在这里

我知道这与你编写的代码并没有太大区别,可能稍微简化了一些。另一种实现相同效果的方法是使用 return 语句而不是 break,并将 for - select 语句定义在一个单独的方法中(假设这样做可以)。以下是示例:

package main

import (
	"fmt"
	"time"
)

func ForSelectMethod(c1 chan string, c2 chan string) {
	for {
		select {
		case msg1, ok := <-c1:
			if !ok {
				return
			}
			{
				fmt.Println(msg1)
			}
		case msg2, ok := <-c2:
			if !ok {
				return
			}
			{
				fmt.Println(msg2)
			}
		}
	}
}

func main() {
	c1 := make(chan string)
	c2 := make(chan string)
	go func() {
		for i := 0; i < 5; i++ {
			time.Sleep(500 * time.Millisecond)
			c1 <- "Every 500 ms"
		}
		close(c1)
	}()
	go func() {
		for i := 0; i < 5; i++ {
			time.Sleep(1000 * time.Millisecond)
			c2 <- "Every 1 s"
		}
		close(c2)
	}()

	ForSelectMethod(c1, c2)

	fmt.Println("For loop exited")
}

Go Playground 链接在这里

英文:

The problem here is that:

> A "break" statement terminates execution of the innermost "for",
> "switch", or "select" statement within the same function.

So we are not able to exit the for loop without additional check after exiting the select statement. I have updated your code a bit to achieve that in simplified manner:

package main

import (
	&quot;fmt&quot;
	&quot;time&quot;
)

func main() {
	c1 := make(chan string)
	c2 := make(chan string)
	go func() {
		for i := 0; i &lt; 5; i++ {
			time.Sleep(500 * time.Millisecond)
			c1 &lt;- &quot;Every 500 ms&quot;
		}
		close(c1)
	}()
	go func() {
		for i := 0; i &lt; 5; i++ {
			time.Sleep(1000 * time.Millisecond)
			c2 &lt;- &quot;Every 1 s&quot;
		}
		close(c2)
	}()
	
	ok := true
	var msg1 string
	var msg2 string
	
	for {
		select {
		case msg1, ok = &lt;-c1:
			if !ok {
				break
			}
			{
				fmt.Println(msg1)
			}
		case msg2, ok = &lt;-c2:
			if !ok {
				break
			}
			{
				fmt.Println(msg2)
			}
		}

		if !ok {
			break
		}
	}

	fmt.Println(&quot;For loop exited&quot;)
}

Go playground link for the above code is here.

I know that this is not very much different from what you have written. Probably slightly more simplified. Another way of achieving the same is using the return statement instead of break and defining the for - select statement in a separate method (assuming that is ok). Find the example below:

package main

import (
	&quot;fmt&quot;
	&quot;time&quot;
)

func ForSelectMethod(c1 chan string, c2 chan string) {
	for {
		select {
		case msg1, ok := &lt;-c1:
			if !ok {
				return
			}
			{
				fmt.Println(msg1)
			}
		case msg2, ok := &lt;-c2:
			if !ok {
				return
			}
			{
				fmt.Println(msg2)
			}
		}
	}
}

func main() {
	c1 := make(chan string)
	c2 := make(chan string)
	go func() {
		for i := 0; i &lt; 5; i++ {
			time.Sleep(500 * time.Millisecond)
			c1 &lt;- &quot;Every 500 ms&quot;
		}
		close(c1)
	}()
	go func() {
		for i := 0; i &lt; 5; i++ {
			time.Sleep(1000 * time.Millisecond)
			c2 &lt;- &quot;Every 1 s&quot;
		}
		close(c2)
	}()

	ForSelectMethod(c1, c2)

	fmt.Println(&quot;For loop exited&quot;)
}

Go Playground link here.

答案2

得分: 1

同样的程序,但是代码行数更少。我认为这样更易读和清晰。

package main

import (
	"time"
)

func stop(tk ...*time.Ticker) {
	for _, t := range tk {
		t.Stop()
	}
}

func main() {
	ta := time.NewTicker(500 * time.Millisecond)
	tb := time.NewTicker(1 * time.Second)

	cta, ctb := 0, 0

	for {
		select {
		case <-ta.C:
			println("每500毫秒")
			cta++
		case <-tb.C:
			println("每1秒")
			ctb++
		}
		if cta == 5 || ctb == 5 {
			stop(ta, tb)
			break
		}
	}
}

样本输出:

每500毫秒
每500毫秒
每1秒
每500毫秒
每500毫秒
每1秒
每500毫秒
英文:

Same program but in fewer lines. I think it's even more readable and clear.

package main

import (
	&quot;time&quot;
)

func stop(tk ...*time.Ticker) {
	for _, t := range tk {
		t.Stop()
	}
}

func main() {
	ta := time.NewTicker(500 * time.Millisecond)
	tb := time.NewTicker(1 * time.Second)

	cta, ctb := 0, 0

	for {
		select {
		case &lt;-ta.C:
			println(&quot;Every 500 ms&quot;)
			cta++
		case &lt;-tb.C:
			println(&quot;Every 1 s&quot;)
			ctb++
		}
		if cta == 5 || ctb == 5 {
			stop(ta, tb)
			break
		}
	}
}

Sample output:

Every 500 ms
Every 500 ms
Every 1 s
Every 500 ms
Every 500 ms
Every 1 s
Every 500 ms

答案3

得分: 0

在这种情况下,常用的方法是使用另一个通道来信号终止。从该通道读取成为 select 语句中的一个情况。通常情况下,当两个写入 goroutine 完成时,您会关闭该通道。没有必要关闭这两个通道。

在您的程序中,当没有通道关闭时,处理结束,并且其他通道的内容被丢弃,从而在某些情况下导致写入该通道的 goroutine 泄漏。如果该 goroutine 尚未完成写入操作,则它将阻塞等待写入一个永远不会被读取的通道。

英文:

A common method to use in this case is to use another channel to signal termination. Reading from that channel becomes one of the cases in the select. Normally, you would close that channel when both writing goroutines are done. There is no need to close the two channels.

In your program, processing ends when none of the channels close, and the contents of the other channel is discarded, effectively leaking the goroutine writing to that channel in some cases. If that goroutine has not finished writing, then it will block waiting to write to a channel that will never be read from.

huangapple
  • 本文由 发表于 2021年5月25日 11:33:11
  • 转载请务必保留本文链接:https://go.coder-hub.com/67681426.html
匿名

发表评论

匿名网友

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

确定