runtime.Goexit 的使用

原文作者: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:

Run a goroutine synchronsly

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.

sync.WaitGroup

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 runfunction and so am dealing with the sync.WaitGroup internally to the mainfunction.

 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!

Channels

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.

runtime.Goexit()

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):

  • If all of the other goroutines run to completion, there'll be no more goroutines to schedule and so the runtime scheduler will panic with a deadlock informing you that Goexit was called and that there are no more goroutines.
  • If any of the other goroutines panic, the application will crash as if any other unrecovered panic had occurred.

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.


版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。

原文发布于微信公众号 - Golang语言社区(Golangweb)

原文发表时间:2018-08-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏JAVA后端开发

activiti集成spring boot的一个怪问题

最近想集成activti到spring boot中,上网找了一下例子,发现很简单,就开干了!

1474
来自专栏Android 开发学习

IntelliJ IDEA spring mvc +mybatis 环境搭建服务器(中)

2353
来自专栏搜云库

Spring Boot 中使用 MyBatis 整合 Druid 多数据源

本文将讲述 spring boot + mybatis + druid 多数据源配置方案。 环境 CentOs7.3 安装 MySQL 5.7.19 二进制版本...

3587
来自专栏程序员的酒和故事

跟Google学写代码--Chromium/base--windows_version源码学习及应用

Chromium是一个伟大的、庞大的开源工程,很多值得我们学习的地方。 前面写道: 《跟Google学写代码–Chromium/base–stl_util源...

3617
来自专栏JavaWeb

项目中Spring 声明式事务使用的一些坑点分析01

7828
来自专栏Golang语言社区

用Go编写的TCP连接监视器库

A TCP connection monitor library written in Go.

2133
来自专栏Hadoop实操

如何使用Sentry为Kafka赋权

8604
来自专栏刘望舒

Android内容服务ContentService原理浅析

ContentService可以看做Android中一个系统级别的消息中心,可以说搭建了一个系统级的观察者模型,APP可以向消息中心注册观察者,选择订阅自己关心...

1395
来自专栏向治洪

ActivityManagerService启动过程分析

之前讲Android的View的绘制原理和流程的时候,讲到过在Android调用setContentView之后,Android调用了一个prepreTravl...

2678
来自专栏owent

GCC 7和LLVM+Clang+libc++abi 4.0的构建脚本

之前的版本发完,有空来更新一下之前的gcc和llvm+clang工具链的编译脚本了。其实GCC 7是才release没多久但是llvm 4.0发布其实有一段时间...

831

扫码关注云+社区

领取腾讯云代金券