退出多个goroutine等待未缓冲通道。

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

exiting multiple go routines waiting on an unbuffered channel

问题

我正在为你翻译以下内容:

我试图同时退出多个 Goroutine。
根据 https://www.godesignpatterns.com/2014/04/exiting-multiple-goroutines-simultaneously.html,有一种明确定义的方法可以实现。

另一种方法如下所示:

package main

import (
	"fmt"
	"time"
)

func main() {
	var inCh chan int = make(chan int, 100)
	var exit chan bool = make(chan bool)

	for i := 0; i < 20; i++ {
		go func(instance int) {
			fmt.Println("In go routine ", instance)
			for {
				select {
				case <-exit:
					fmt.Println("Exit received from ", instance)
					exit <- true
					return
				case value := <-inCh:
					fmt.Println("Value=", value)
				}
			}
		}(i)
	}

	time.Sleep(1 * time.Second)

	exit <- true
	<-exit   // 最终退出
	fmt.Println("Final exit")
}

但是我感到困惑,我真的不明白为什么未缓冲的通道在最后作为最后一条语句执行。
实际上,我有 20 个 Goroutine 监听退出通道。随机地,其中一个将接收到退出信号并将其发送给另一个。
为什么总是在 Goroutine 中进行接收,并且只有当它们全部完成时,才会执行带有注释 "// Final Exit" 的通道接收?

如果有人能给我解释一下,我将非常感激。

英文:

I was trying to exit multiple Goroutines Simultaneously.
According to https://www.godesignpatterns.com/2014/04/exiting-multiple-goroutines-simultaneously.html
there is a well defined way to do it.

Another approach i see is the following

package main

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

func main() {
	var inCh chan int = make(chan int, 100)
	var exit chan bool = make(chan bool)

	for i := 0; i &lt; 20; i++ {
		go func(instance int) {
			fmt.Println(&quot;In go routine &quot;, instance)
			for {
				select {
				case &lt;-exit:
					fmt.Println(&quot;Exit received from &quot;, instance)
					exit &lt;- true
					return
				case value := &lt;-inCh:
					fmt.Println(&quot;Value=&quot;, value)
				}
			}
		}(i)
	}

	time.Sleep(1 * time.Second)

	exit &lt;- true
	&lt;-exit   // Final exit
	fmt.Println(&quot;Final exit&quot;)
}

But I am confused and i really don't get it why the unbuffered channel at the end is executed as a last statement.
In reality I have 20 go routines listening for exit channel. Randomly one will receive it and send it to another.
Why always the reception within go routines is taking place and only when all of them are finished the channel reception with comment "// Final Exit" will execute ?

I will really appreciate if someone can give me an explanation.

答案1

得分: 2

在链接的文章中,使用close()来取消操作。

所讨论的代码不能保证正常工作。以下是一个导致失败的场景:

  1. 一个goroutine准备好从exit接收数据,而其他所有的goroutine都在其他地方忙碌。
  2. 主函数发送的值被准备好的goroutine接收。
  3. 那个goroutine向exit发送一个值,该值被main()接收。

其他的goroutine不会退出,因为没有更多的值被发送到exit。可以查看这个playground示例,它使用time.Seep来引发这个问题的场景。

为什么只有在所有的goroutine都完成后,才会执行带有注释"// Final Exit"的通道接收操作?

程序的执行方式好像通道维护了一个有序的等待goroutine队列,但是规范中没有保证这种行为。即使通道有一个有序的队列,如果一个goroutine在等待接收exit之外做其他事情,程序仍然可能遇到上述的场景。

英文:

Use close() for cancelation as shown in the linked article.

The code in question is not guaranteed to work. Here's a scenario where it fails:

  1. One goroutine is ready to receive from exit. All other goroutines busy somewhere else.
  2. The value sent by main is received by the ready goroutine.
  3. That goroutine sends a value to exit that is received by main().

The other goroutines do not exit because no more values are sent to exit. See this playground example that uses time.Seep to induce the problem scenario.

> Why always the reception within go routines is taking place and only when all of them are finished the channel reception with comment "// Final Exit" will execute ?

The program executes as if the channel maintains an ordered queue of waiting goroutines, but there's nothing in the specification that guarantees that behavior. Even if the channel has an ordered queue, the program can encounter the scenario above if a goroutine is doing something other than waiting on receive from exit.

答案2

得分: 1

如果你注意到程序的输出

在go协程 6中
在go协程 0中
在go协程 7中
.
.
从6接收到退出信号
从0接收到退出信号
从7接收到退出信号
.
.
最终退出

它们被调用的顺序与它们启动的顺序相同(或几乎相同)。如果没有任何Go协程忙碌,将使用注册的第一个协程。这只是运行时的一种实现,我不会依赖这种行为。

你的最终退出是最后一个被监听的通道,所以它最后被使用。

如果你在循环之后移除time.Sleep,你的最终退出将几乎立即被调用,大部分的Go协程将不会接收到退出信号。

没有time.Sleep的输出(每次运行结果可能不同)

在go协程 0中
从0接收到退出信号
在go协程 1中
在go协程 2中
在go协程 3中
在go协程 4中
在go协程 5中
在go协程 6中
在go协程 7中
在go协程 14中
在go协程 15中
在go协程 16中
在go协程 17中
在go协程 18中
在go协程 19中
最终退出
英文:

If you notice the output of you program

In go routine  6
In go routine  0
In go routine  7
.
.
Exit received from  6
Exit received from  0
Exit received from  7
.
.
Final exit

They get called in the same( or almost the same) order of how they were started. If none of your Go routines are busy the first one registered will be used. This is just an implementation of the runtime and I would not count on this behavior.

Your final exit was the last channel to be listened on so it is used last.

If you remove the time.Sleep after your loop your final exit will get called almost immediately and most of your go routines will not receive the exit signal

Output with out time.Sleep (will very between runs)

In go routine  0
Exit received from  0
In go routine  1
In go routine  2
In go routine  3
In go routine  4
In go routine  5
In go routine  6
In go routine  7
In go routine  14
In go routine  15
In go routine  16
In go routine  17
In go routine  18
In go routine  19
Final exit

答案3

得分: 0

考虑这个轻微的修改。

package main

import (
    "fmt"
)

func main() {
    var exit chan int = make(chan int)
    var workers = 20
    for i := 0; i < workers; i++ {
        go func(instance int) {
            fmt.Println("在go协程中", instance)
            for {
                select {
                case i := <-exit:
                    fmt.Println("从", instance, "接收到退出信号", i)
                    exit <- i - 1
                    return
                }
            }
        }(i)
    }
    exit <- workers
    fmt.Println("最终退出:", <-exit)
}

在这里,我做了三件事:首先,为了简洁起见,我删除了未使用的通道。其次,我删除了休眠。第三,我将exit通道更改为一个int通道,每次通过时递减。如果我传入的是工作线程的数量,那么从“最终”消息返回的任何值都表示丢失的工作线程。

这是一个示例运行:

% go run t.go
在go协程中 8
在go协程中 5
在go协程中 0
在go协程中 2
8 接收到退出信号 20
5 接收到退出信号 19
最终退出: 18
在go协程中 13

main调用time.Sleep时,直到休眠结束才会被调度。其他的goroutine都有这段时间来设置它们的通道读取器。我只能假设,因为我找不到任何地方写明,通道读取器可能会按照大致按时间顺序排队 - 因此,sleep保证了main的读取器是最后一个。

如果这是一致的行为,那么它肯定是不可靠的。

请参阅https://stackoverflow.com/questions/15715605/multiple-goroutines-listening-on-one-channel,了解更多关于这个问题的思考。

英文:

Consider this slight modification.

package main

import (
    &quot;fmt&quot;
)

func main() {
    var exit chan int = make(chan int)
		var workers = 20
    for i := 0; i &lt; workers; i++ {
        go func(instance int) {
            fmt.Println(&quot;In go routine &quot;, instance)
            for {
                select {
				case i := &lt;-exit:
                    fmt.Println(&quot;Exit&quot;, i, &quot;received from &quot;, instance)
                    exit &lt;- i-1
                    return
                }
            }
        }(i)
    }
    exit &lt;- workers
	fmt.Println(&quot;Final exit:&quot;, &lt;-exit)
}

Here, I've done 3 things: First, I removed the unused channel, for brevity. Second, I removed the sleep. third, I changed the exit channel to an int channel that is decremented by every pass. If I pass the number of workers in, any value other than 0 from the "Final" message indicates dropped workers.

Here's one example run:

% go run t.go
In go routine  8
In go routine  5
In go routine  0
In go routine  2
Exit 20 received from  8
Exit 19 received from  5
Final exit: 18
In go routine  13

When main calls time.Sleep it doesn't get scheduled until the sleep is over. The other goroutines all have this time to set up their channel readers. I can only assume, because I can't find it written anywhere, that channel readers are likely to be queued in roughly chronological error - thus, the sleep guarantees that main's reader is the last.

If this is consistent behavior, it certainly isn't reliable.

See https://stackoverflow.com/questions/15715605/multiple-goroutines-listening-on-one-channel for many more thoughts on this.

huangapple
  • 本文由 发表于 2021年10月9日 05:35:18
  • 转载请务必保留本文链接:https://go.coder-hub.com/69501945.html
匿名

发表评论

匿名网友

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

确定