優雅地等待子協程執行完畢

pardon110發表於2019-10-14

goroutine模擬了執行緒級別的返場的能力,但它的執行需要主協程給機會。一般的作法用sleep,chan阻塞,看起來讓人不爽,本文介紹sync.WaitGroup 型別結合 defer 的特性,給出優雅的解決方案。

緣起

下面這段程式碼眾所周知不會打招呼

....

func main(){
    go sayHi(){
        fmt.Println("say hello......")
    }()
    fmt.Println("main groutine....")
}

等待

上述程式碼不會講hello,在於主協程main無等待(阻塞),子協程sayHi沒有露臉的機會。
為了讓子協程sayHai上場,通常在主協程末了加這麼一句,讓它睡會兒

time.Sleep(1e9)

但想想不科學,如果子協程在它睡期間,沒能完成任務,超時了,子協程仍然打不了招呼。又或者子協程完成了,主協程還在睡,豈不是主執行緒不作為?睡的時間多久怎樣才合理...

通道

用通道可解決阻塞時間合理性質疑。
若啟用了多個子協程,可以這樣實現主協程等待子協程執行完畢並退出的:宣告一個和子協程數量一致的通道陣列,然後為每個子協程分配一個通道元素,在子協程執行完畢時向對應的通道傳送資料;然後在主協程中,依次讀取這些通道接收子協程傳送的資料,只有所有通道都接收到資料才會退出主協程。
程式碼看起來像這樣

chs := make([]chan int, 10)
for i := 0; i < 10; i++ {
    chs[i] = make(chan int)
    go add(1, i, chs[i])
}
for _, ch := range chs {
    <- ch
}

感覺這樣的實現有點蹩腳,不夠優雅,於是 sync.waitGroup 入場了

WaitGroup 型別

sync.WaitGroup 型別是併發安全的,提供了以下三個方法:

func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Wait()
  • Add:WaitGroup 型別有一個計數器,預設值是0,通常通過個方法來標記需要等待的子協程數量
  • Done:當某個子協程執行完畢後,可以通過 Done 方法標記已完成,常用 defer 語句來呼叫
  • Wait 阻塞當前協程,直到對應 WaitGroup 型別例項的計數器值歸零

程式碼

至此,相信你已經明白了,可組合使用 sync.WaitGroup 型別提供的方法,來替代之前通道中等待子協程執行完畢。
優雅等待子協程執行完畢程式碼如下

package main

import (
    "fmt"
    "sync"
)

func add_num(a, b int, done func()) {
    defer done()
    c := a + b
    fmt.Printf("%d + %d = %d\n", a, b, c)
}

func main() {
    var wg sync.WaitGroup
    wg.Add(10)
    for i := 0; i < 10; i++ {
        go add_num(i, 1, wg.Done)
    }
    wg.Wait()
}

補充

其實runtime 執行時預設是隻開啟一個邏輯處理器,如果不想用通道,也不阻塞,可以考慮另開戰場,比如 像下面這樣

runtime.GOMAXPROCS(n)       // 設定使用多核邏輯處理器,n大於1

相關文章