Goroutine 是 Golang 中非常有用的功能,但是在使用中我们经常碰到下面的场景:如果希望等待当前的 goroutine 执行完成,然后再接着往下执行,该怎么办?本文尝试介绍这类问题的解决方法。
让我们运行下面的代码,并关注输出的结果:
复制
package main
import (
"time"
"fmt"
)
func say(s string) {
for i := 0; i < 3; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("hello world")
fmt.Println("over!")
}
输出的结果为:over! 因为 goroutine 以非阻塞的方式执行,它们会随着程序(主线程)的结束而消亡,所以程序输出字符串 "over!" 就退出了,这可不是我们想要的结果。
要解决上面的问题,最简单、直接的方式就是通过 Sleep 函数死等 goroutine 执行完成:
复制
func main() {
go say("hello world")
time.Sleep(1000 * time.Millisecond)
fmt.Println("over!")
}
运行修改后的程序,结果如下:
hello world
hello world
hello world
over!
结果符合预期,但是太 low 了,我们不知道实际执行中应该等待多长时间,所以不能接受这个方案!
通过 channel 也可以达到等待 goroutine 结束的目的,运行下面的代码:
复制
func main() {
done := make(chan bool)
go func() {
for i := 0; i < 3; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println("hello world")
}
done <- true
}()
<-done
fmt.Println("over!")
}
输出的结果也是:
hello world
hello world
hello world
over!
这种方法的特点是执行多少次 done <- true 就得执行多少次 <-done,所以也不是优雅的解决方式。
Golang 官方在 sync 包中提供了 WaitGroup 类型来解决这个问题。其文档描述如下:
使用方法可以总结为下面几点:
运行下面的代码:
复制
package main
import (
"time"
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
say2("hello", &wg)
say2("world", &wg)
fmt.Println("over!")
}
func say2(s string, waitGroup *sync.WaitGroup) {
defer waitGroup.Done()
for i := 0; i < 3; i++ {
fmt.Println(s)
}
}
输出的结果如下:
hello
hello
hello
world
world
world
over!
下面是一个稍稍真实一点的例子,检查请求网站的返回状态。如果要在收到所有的结果后进一步处理这些返回状态,就需要等待所有的请求结果返回:
复制
package main
import (
"fmt"
"sync"
"net/http"
)
func main() {
var urls = []string{
"https://www.baidu.com/",
"https://www.cnblogs.com/",
}
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go fetch(url, &wg)
}
wg.Wait()
}
func fetch(url string, wg *sync.WaitGroup) (string, error) {
defer wg.Done()
resp, err := http.Get(url)
if err != nil {
fmt.Println(err)
return "", err
}
fmt.Println(resp.Status)
return resp.Status, nil
}
运行上面的代码,输出的结果如下:
200 OK
200 OK
上面的场景是等待多个小任务都执行完毕之后退出。下面考虑这种场景:有一个持续性的任务,一般是常驻的不退出,比如说监控任务。但有时可能因为特殊需求,需要根据条件判断后让其退出。因为该监控任务和其他任务没有数据连接(channel之类的),但靠sync.WaitGroup是不能使其退出的。 很自然的,我们能想到应该通过一个媒介,来通知监控任务退出。全局变量可以充当这个媒介,但鉴于安全性很差,这里不予考虑(代码中一般来说不允许出现全局变量)。
复制
func main() {
stop := make(chan bool)
go func() {
for {
select {
case <-stop:
fmt.Println("监控退出,停止了...")
return
default:
fmt.Println("goroutine监控中...")
time.Sleep(2 * time.Second)
}
}
}()
time.Sleep(10 * time.Second)
fmt.Println("可以了,通知监控停止")
stop <- true
//为了检测监控过是否停止,如果没有监控输出,就表示停止了
time.Sleep(5 * time.Second)
}
输出结果:
复制
goroutine监控中...
goroutine监控中...
goroutine监控中...
goroutine监控中...
goroutine监控中...
可以了,通知监控停止
监控退出,停止了...
这种方式还是比较优雅的,但还是有很大的局限性。如果我们需要控制很多的goruntine结束怎么办,难道我们要搞出很多个chan?又或者子goruntine中又衍生出更多的子goruntine怎么办?如果一层层的无穷尽的 goroutine 呢?这就非常复杂了,即使我们定义很多 chan 也很难解决这个问题,因为 goroutine 的关系链就导致了这种场景非常复杂。
上面说的这种场景是存在的,比如一个网络请求 Request,每个 Request 都需要开启一个 goroutine 做一些事情,这些 goroutine 又可能会开启其它的 goroutine。所以我们需要一种可以跟踪 goroutine 的方案,才可以达到控制它们的目的,这就是Go语言为我们提供的 Context,称之为上下文非常贴切,它就是 goroutine 的上下文。
复制
func main() {
ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("监控退出,停止了...")
return
default:
fmt.Println("goroutine监控中...")
time.Sleep(2 * time.Second)
}
}
}(ctx)
time.Sleep(10 * time.Second)
fmt.Println("可以了,通知监控停止")
cancel()
//为了检测监控过是否停止,如果没有监控输出,就表示停止了
time.Sleep(5 * time.Second)
}
复制
func main() {
ctx, cancel := context.WithCancel(context.Background())
go watch(ctx, "【监控1】")
go watch(ctx, "【监控2】")
go watch(ctx, "【监控3】")
time.Sleep(10 * time.Second)
fmt.Println("可以了,通知监控停止")
cancel()
//为了检测监控过是否停止,如果没有监控输出,就表示停止了
time.Sleep(5 * time.Second)
}
func watch(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name, "监控退出,停止了...")
return
default:
fmt.Println(name, "goroutine监控中...")
time.Sleep(2 * time.Second)
}
}
}