天天看點

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