英文:
What happens to unfinished goroutines when the main/parent goroutine exits or returns?
问题
我正在阅读《Go编程语言》这本书,在第8.4章中有一个例子。
func mirroredQuery() string{
responses := make(chan string, 3)
go func() { responses <- request("asia.gopl.io") }()
go func() { responses <- request("americas.gopl.io") }()
go func() { responses <- request("europe.gopl.io") }()
return <- responses // 返回最快的响应
}
还有这个注释:
如果我们使用的是无缓冲通道,那么两个较慢的goroutine将会被阻塞,试图将它们的响应发送到一个没有goroutine会接收的通道上。
这个注释本身是有道理的。但是在缓冲情况下,当mirroredQuery
返回时,两个慢的goroutine会发生什么?它们是否继续运行直到完成,还是被取消了?
编辑:我理解如果主goroutine退出,那么无论这两个较慢的goroutine是否正在运行,它们都会"消失"。但是如果主goroutine仍在运行,mirroredQuery()
已经返回,那么这两个慢的goroutine会继续运行到结束吗?基本上,responses
在mirroredQuery
返回后是否仍然存在?如果是这样,那么从原则上讲,这两个慢的goroutine可以完成;如果不是,那么我们仍然会有类似无缓冲情况下的泄漏问题。
英文:
I am reading the go programming language book and there is this example in Chapter 8.4
func mirroredQuery() string{
responses := make(chan string, 3)
go func() { responses <- request("asia.gopl.io") }()
go func() { responses <- request("americas.gopl.io") }()
go func() { responses <- request("europe.gopl.io") }()
return <- responses // return the quickest response
}
There is also this comment
> Had we used an unbuffered channel, the two slower goroutines would have gotten stuck trying to send their responses on a channel from which no goroutine will ever receive.
This comment itself makes sense. But what happens to the two slow goroutines when mirroredQuery
returns in the buffered case? Do they still run to finish or get cancelled?
EDIT: I understand that if the main goroutnine exits, then the 2 slower gorountines will 'evaporate' no matter they are running or not. But what if the main goroutine is still running, mirroredQuery()
has already returned, would the 2 slow goroutines run to end? Basically, does responses
still exist after mirroredQuery
returns? If so, then it seems the 2 slow goroutines can finish in principle; if not, then we still have leakage just like the unbuffered case?
答案1
得分: 11
当main goroutine返回时,整个运行时系统会突然退出。因此,任何被阻塞在无缓冲或满通道上等待发送的goroutine都会停止存在。它们不会被"取消",也不会运行或等待。可以将其视为闪光纸被点燃的过程。
可以将这称为goroutine泄漏,就像在程序终止之前未关闭或释放的任何资源(如打开的文件)被称为"泄漏"一样。但由于整个进程终止了,没有留下任何东西。这里没有真正的"泄漏"。虽然不是一个整洁的清理过程,但系统会进行清理。
这是一个示例的Playground链接:https://go.dev/play/p/tAVQqs_3Hpc
(如果使用了Go系统本身未定义的东西,可能会导致各种泄漏。例如,在旧的System V共享内存世界中,可以创建共享内存段(shm_open
),如果您从未关闭和取消链接它们,它们将持续存在。这是有意设计的:它们的行为很像文件系统中的文件,只是它们存在于内存中,而不是在某种磁盘驱动器或其他地方。但这远远超出了正常的日常Go编程范畴。)
关于您的编辑:如果main goroutine尚未退出,即程序仍在运行,则其他goroutine将继续运行(或等待),直到它们完成所有任务并返回,或者执行某些导致它们退出的操作(例如调用runtime.Goexit
,或者执行某些导致恐慌的操作)。在这种情况下,它们会等待响应,然后将响应发送到通道中,然后返回。假设它们收到了响应,它们会将响应放入通道中。假设将响应放入通道中成功(不会发生恐慌且不会阻塞),它们将返回。一旦返回,它们就完成了并消失了。通道本身会持续存在并保存字符串:这是一个资源泄漏,尽管在玩具程序中是一个较小的泄漏。
如果没有对通道本身的引用,通道本身将被垃圾回收,以及其中的字符串;这样就清理了泄漏的资源。由于我们假设mirroredQuery
已经返回,并且此时派生的goroutine中的最后一个也已经返回,这是对通道的最后一个引用,所以现在可以对通道进行垃圾回收。(是否以及何时发生这种情况取决于运行时。)在这些goroutine中的最后一个完成之前,仍然至少有一个对通道的引用,防止通道(以及其中的字符串)被垃圾回收。
如果通道是无缓冲的,那么两个"失败"的goroutine将在尝试发送到通道时被阻塞。这将导致这些goroutine保持存在,进而导致通道保持存在,进而导致资源保持分配,直到整个程序终止。所以这是"不好"的。
如果mirroredQuery
关闭了通道,那么两个"失败"的goroutine可以尝试在关闭的通道上发送,这将导致它们调用恐慌代码,从而终止程序。这也是"不好"的。实现所需结果的最简单代码是使通道具有缓冲区。
如果其中一个goroutine等待(等待响应)几年,那么它将保持这些"泄漏"的资源几年。这也是"不好"的(稍微有点),所以我们希望确保它们不会永远等待。但在一个小的演示程序中,这是不切实际的。
英文:
When the main goroutine returns, the entire runtime system quits, rather abruptly. Hence any goroutines that are stuck waiting to send on an unbuffered or full channel simply ... cease to exist. They're not canceled, nor do they run, nor do they wait. Think of it as the flash paper being set on fire.
One can call this a goroutine leak, the same way one can refer to any resources (such as open files) not closed-or-freed before a program terminates a "leak". But since the entire process terminates, there's nothing left. There's no real leak here. It's not a tidy cleanup, but the system does clean up.
Here's a Playground link, as an example.
(If you make use of things not defined by the Go system itself, you could get various leaks that way. For instance, in the old System V Shared Memory world, you can create shared memory segments (shm_open
) and if you never close and unlink them, they persist. This is by design: they're meant to act a lot like files in a file system, except that they exist in memory, rather than on some sort of disk drive or whatever. But this is far outside normal everyday Go programming.)
Re your edit: if the main goroutine has not exited, so that the program is still running, the other goroutines continue to run (or wait) until they run out of things to do and return themselves, or do something that causes them to exit (such as call runtime.Goexit
, or do something that causes a panic). In this case, that's: wait for a response, then send the response into the channel, then return. Assuming they get a response, they'll put the response into the channel. Assuming that putting the response into the channel works (does not panic and not block), they will then return. Having returned, they are done and they evaporate. The channel itself persists and holds the strings: this is a resource leak, albeit a minor one, especially in a toy program.
If there are no references left to the channel itself, the channel itself will be garbage-collected, along with the strings in it; this cleans up the leaked resources. Since we assume that mirroredQuery
has returned, and that at this point the last of the spun-off goroutines has also returned, that's the last reference to the channel, so now the channel can be GCed. (Whether and when this happens is up to the runtime.) Until the last of these goroutines finishes, there's still at least one reference to the channel, preventing the channel (and hence the strings) from being GCed.
Had the channel been unbuffered, the two "losing" goroutines would block in the attempt to send into the channel. That would cause those goroutines to remain, which in turn would cause the channel to remain, which in turn would cause the resources to remain allocated until the program as a whole terminates. So that would be "bad".
Had mirroredQuery
closed the channel, the two "losing" goroutines could attempt to send on a closed channel, which would cause them to invoke the panic code, which would kill the program. That too would be "bad". The simplest code that achieves the desired result is to make the channel buffered.
Should one of the goroutines wait (for a response) for several years, that would hold those "leaked" resources for all those years. That would also be "bad" (slightly), so we'd want to make sure that they don't wait forever. But that's impractical in a small demonstration program.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论