Golang 入門 : 等待 goroutine 完成任務

sparkdev發表於2019-05-24

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

相關文章