What happens to unfinished goroutines when the main/parent goroutine exits or returns?

Issue

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?

Solution

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.

Answered By – torek

Answer Checked By – Timothy Miller (GoLangFix Admin)

Leave a Reply

Your email address will not be published.