Golang 入门 : 等待 goroutine 完成任务

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 函数等待

要解决上面的问题,最简单、直接的方式就是通过 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

通过 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 类型来解决这个问题。其文档描述如下:

A WaitGroup waits for a collection of goroutines to finish. The main goroutine calls Add to set the number of goroutines to wait for. Then each of the goroutines runs and calls Done when finished. At the same time, Wait can be used to block until all goroutines have finished.

大意为:WaitGroup 用来等待单个或多个 goroutines 执行结束。在主逻辑中使用 WaitGroup 的 Add 方法设置需要等待的 goroutines 的数量。在每个 goroutine 执行的函数中,需要调用 WaitGroup 的 Done 方法。最后在主逻辑中调用 WaitGroup 的 Wait 方法进行阻塞等待,直到所有 goroutine 执行完成。
使用方法可以总结为下面几点:

  1. 创建一个 WaitGroup 实例,比如名称为:wg
  2. 调用 wg.Add(n),其中 n 是等待的 goroutine 的数量
  3. 在每个 goroutine 运行的函数中执行 defer wg.Done()
  4. 调用 wg.Wait() 阻塞主逻辑

运行下面的代码:

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) {
    resp, err := http.Get(url)
    if err != nil {
        fmt.Println(err)
        return "", err
    }
    wg.Done()
    fmt.Println(resp.Status)
    return resp.Status, nil
}

运行上面的代码,输出的结果如下:
200 OK
200 OK

参考:
How to Wait for All Goroutines to Finish Executing Before Continuing
Go WaitGroup Tutorial

posted @ 2019-05-24 13:07 sparkdev 阅读(...) 评论(...) 编辑 收藏