I'm writing some golang concurrency codes with goroutines and channels, suspecting that my code may cause goroutine leaks. My situation is similar to the following code, or you can open this go playground link.
func main() {
numCount := 3
numChan := make(chan int)
for i := 0; i < numCount; i++ {
go func(num int) {
fmt.Printf("Adding num: %d to chan
", num)
numChan <- num
fmt.Printf("Adding num: %d to chan Done
", num)
}(i)
}
time.Sleep(time.Second)
panic("Goroutine Resource Leak Test")
}
In my opinion, when the main goroutine returns, three goroutines are blocked sending to the unbuffered channel and there will be goroutine leak. This post goroutine leak with buffered channel in Go also suggests that So only if the channel was unbuffered the leak would occur
.
The Go Programming Language suggests that:
There’s a handy trick we can use during testing: if instead of returning from main in the event of cancellation, we execute a call to panic, then the runtime will dump the stack of every goroutine in the program. If the main goroutine is the only one left, then it has cleaned up after itself. But if other goroutines remain, they may not have been properly canceled, or perhaps they have been canceled but the cancellation takes time; a little investigation may be worthwhile. The panic dump often contains sufficient information to distinguish these cases.
Therefore, I added panic("Goroutine Resource Leak Test")
to the end of the main function to verify my assumption. However, the panic dump only contains main goroutine, that is, there is no resource leak.
Adding num: 0 to chan
Adding num: 1 to chan
Adding num: 2 to chan
panic: Goroutine Resource Leak Test
goroutine 1 [running]:
main.main()
/tmp/sandbox011109649/prog.go:21 +0xc0
Can someone help explain
Any suggestion will be appreciated, thanks in advance!
The problem with your code is twofold.
First, there is, theoretically, a goroutine leak since any attempt to send a value to a channel with zero capacity (an unbuffered channel or a filled up buffered channel) blocks the sending goroutine until a receive operation is done on that channel.
So, yes, by definition of how channels work, all your three goroutines will be blocked in the numChan <- num
statement.
Second, since some revision of Go, an unhandled panic
by default dumps only the stack trace of the panicking goroutine. If you wish to dump the stacks of all the active goroutines, you'd have to tweak the runtime — from the documentation of the package runtime
:
The
GOTRACEBACK
variable controls the amount of output generated when a Go program fails due to an unrecovered panic or an unexpected runtime condition. By default, a failure prints a stack trace for the current goroutine, eliding functions internal to the run-time system, and then exits with exit code 2. The failure prints stack traces for all goroutines if there is no current goroutine or the failure is internal to the run-time.GOTRACEBACK=none
omits the goroutine stack traces entirely.GOTRACEBACK=single
(the default) behaves as described above.GOTRACEBACK=all
adds stack traces for all user-created goroutines.GOTRACEBACK=system
is like “all” but adds stack frames for run-time functions and shows goroutines created internally by the run-time.GOTRACEBACK=crash
is like “system” but crashes in an operating system-specific manner instead of exiting. For example, on Unix systems, the crash raisesSIGABRT
to trigger a core dump. For historical reasons, theGOTRACEBACK
settings 0, 1, and 2 are synonyms for none, all, and system, respectively.The runtime/debug
package'sSetTraceback
function allows increasing the amount of output at run time, but it cannot reduce the amount below that specified by the environment variable. See https://golang.org/pkg/runtime/debug/#SetTraceback.
Also note that you must not ever use timers for (simulating) synchronization: in a toy example this might work but in real life nothing prevents your three goroutines from not having a chance to be run during the time span your main goroutine spent in the call to time.Sleep
— so the outcome might be that any number of spawned goroutines had run: from 0 to 3.
Add there the fact that when main
exits the runtime merely kills all the outstanding active goroutines, and the result of the test might be surprising at best.
Hence a proper solution would be to
package main
import (
"fmt"
"log"
"runtime"
)
func dumpStacks() {
buf := make([]byte, 32 * 1024)
n := runtime.Stack(buf, true)
log.Println(string(buf[:n]))
}
func main() {
numCount := 3
numChan := make(chan int, numCount)
for i := 0; i < numCount; i++ {
go func(num int) {
fmt.Printf("Adding num: %d to chan
", num)
numChan <- num
fmt.Printf("Adding num: %d to chan Done
", num)
}(i)
}
dumpStacks()
for i := 0; i < numCount; i++ {
<-numChan
}
}