原文作者:Rob Reid
If you've ever needed to kick off multiple goroutines from func main
, you'd have probably noticed that the main goroutine isn't likely to hang around long enough for the other goroutines to finish:
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func main() {
9 go run(1, "A")
10 go run(5, "B")
11}
12
13func run(iter int, name string) {
14 for i := 0; i < iter; i++ {
15 time.Sleep(time.Second)
16 fmt.Println(name)
17 }
18}
It'll come as no surprise that this program outputs nothing and exits with an exit code of 0. The nature of goroutines is to be asynchronous, so while the "A" and "B" goroutines are being scheduled, the main goroutine is running to completion and hence closing our application.
There are many ways to run both the "A" and "B" goroutines to completion, some more involved than others. Here are a few:
If you're confident that one of your goroutines will run for longer than the other, you could simply call one of the routines synchronously and hope for the best:
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func main() {
9 go run(1, "A")
10 run(5, "B")
11}
12
13func run(iter int, name string) {
14 for i := 0; i < iter; i++ {
15 time.Sleep(time.Second)
16 fmt.Println(name)
17 }
18}
1$ go run main.go
2B
3A
4B
5B
6B
7B
8<EXIT 0>
This of course falls down if the goroutine you're waiting on takes less time than the other, as the only thing keeping your application running is the goroutine you're running synchronously:
1go run(5, "A")
2run(1, "B")
1$ go run main.go
2B
3<EXIT 0>
...so not a workable solution unless you're running things like long-running web servers.
A more elegant solution would be to use sync.WaitGroup
configured with a delta equal to the number of goroutines you're spawning. Your application will run to completion after all of the goroutines exit.
In the following example, I'm assuming that we don't have access to the run
function and so am dealing with the sync.WaitGroup
internally to the main
function.
1package main
2
3import (
4 "fmt"
5 "sync"
6 "time"
7)
8
9func main() {
10 var wg sync.WaitGroup
11 wg.Add(2)
12 go func() {
13 defer wg.Done()
14 run(1, "A")
15 }()
16 go func() {
17 defer wg.Done()
18 run(5, "B")
19 }()
20 wg.Wait()
21}
22
23func run(iter int, name string) {
24 for i := 0; i < iter; i++ {
25 time.Sleep(time.Second)
26 fmt.Println(name)
27 }
28}
1$ go run main.go
2B
3A
4B
5B
6B
7B
8<EXIT 0>
This is a more elegant solution to the hit-and-hope solution as it leaves nothing to chance. As with the above example, you'll likely want/need to keep the wait group code within your main
function, so provided you don't mind polluting it with synchronisation code, you're all good.
If you need to add/remove a goroutine, don't forget to increment the delta, or your application won't behave as expected!
It's also possible to use channels to acheive this behaviour, by creating a buffered channel with the same size as the delta you initialised the sync.WaitGroup
with.
In the below example, I once again assume no access to the run
function and keep all synchronisation logic in the main
function:
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func main() {
9 done := make(chan struct{})
10
11 go func() {
12 defer func() { done <- struct{}{} }()
13 run(1, "A")
14 }()
15
16 go func() {
17 defer func() { done <- struct{}{} }()
18 run(5, "B")
19 }()
20
21 for i := 0; i < 2; i++ {
22 <-done
23 }
24}
25
26func run(iter int, name string) {
27 for i := 0; i < iter; i++ {
28 time.Sleep(time.Second)
29 fmt.Println(name)
30 }
31}
1$ go run main.go
2B
3A
4B
5B
6B
7B
The obvious added complexity and the fact that the synchronisation code needs to be updated if a goroutine needs to be added/removed detract from the elegance of this approach. Forget to increment your channel's reader delta and your application will exit earlier than expected and forget to decrement it and it'll crash with a deadlock.
Another solution is to use the runtime package's Goexit
function. This function executes all deferred statements and then stops the calling goroutine, leaving all other goroutines running. Like all other goroutines, Goexit
can be called from the main goroutine to kill it and allow other goroutines to continue running.
Exit wise, once the Goexit
call is in place, your application can only fail. If your application is running in an orchestrated environment like Kubernetes (or you're just happy to tolerate non-zero exit codes), this might be absolutely fine but it's something to be aware of.
There are two ways your application can now exit (both resulting in an exit code of 2):
Goexit
was called and that there are no more goroutines.With all the doom and gloom out the way, let's take a look at the code:
1package main
2
3import (
4 "fmt"
5 "runtime"
6 "time"
7)
8
9func main() {
10 go run(1, "A")
11 go run(5, "B")
12
13 runtime.Goexit()
14}
15
16func run(iter int, name string) {
17 for i := 0; i < iter; i++ {
18 time.Sleep(time.Second)
19 fmt.Println(name)
20 }
21}
1$ go run main.go
2B
3A
4B
5B
6B
7B
8fatal error: no goroutines (main called runtime.Goexit) - deadlock!
9<STACK OMITTED>
10<EXIT 2>
Succinct, if a little scary!
This solution understandably won't be for everyone, especially if you're working with inexperienced gophers (for reasons of sheer confusion, "my application keeps failing" and "nice, I'll use this everywhere") but it's nevertheless an interesting one, if only from an academic perspective.
版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。