runtime.Goexit 的使用

简介:

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.

原文发布时间为:2018-08-22
本文来自云栖社区合作伙伴“Golang语言社区”,了解相关信息可以关注“Golang语言社区”。

相关文章
|
Kubernetes API 调度
Container Runtime CDI与NRI介绍
CDI介绍什么是CDICDI(Container Device Interface)是Container Runtimes支持挂载第三方设备(比如:GPU、FPGA等)机制。它引入了设备作为资源的抽象概念,这类设备由一个完全限定的名称唯一指定,该名称由设备商ID,设备类别与一个设备类别下的一个唯一名称组成,格式如下:vendor.com/class=unique_name设备商ID和设备类型(ve
3602 1
Container Runtime CDI与NRI介绍
|
存储 编译器 API
Runtime的使用
Runtime 又叫运行时,是一套底层的 C 语言 API,其为 iOS 内部的核心之一,我们平时编写的 OC 代码,底层都是基于它来实现的。
263 0
Runtime的使用
|
编译器 C语言 iOS开发
Runtime详解及应用
动态语言:编译时确定变量的数据类型。 静态语言:运行时确定变得的数据类型。
293 0
Runtime详解及应用
|
Java Android开发
Eclipse中项目报Target runtime com.genuitec.runtime.generic.jee60 is not defined异常的解决方法
Eclipse中项目报Target runtime com.genuitec.runtime.generic.jee60 is not defined异常的解决
523 0
Eclipse中项目报Target runtime com.genuitec.runtime.generic.jee60 is not defined异常的解决方法
|
设计模式 Java 开发者
Runtime 相关说明 | 学习笔记
快速学习 Runtime 相关说明。
128 0
|
C++
C++ runtime sample
本文演示如何利用函数计算的自定义Runtime功能来运行C++代码
1762 0
|
缓存 iOS开发 编译器
Runtime那些事
Runtime介绍。
2043 0
|
Windows 设计模式
|
存储 JSON 数据格式