Golang的WaitGroup陷阱

sync.WaitGroup是併發環境中,一個至關經常使用的數據結構,用來等待全部協程的結束,在寫代碼的時候都是按着例子的樣子寫的,也沒用深究過它的使用。前幾日想着能不能在協程中執行Add()函數,答案是不能,這裏介紹下。golang

陷阱在WaitGroup的3個函數的調用順序上。先回顧下3個函數的功能:bash

  1. Add(delta int):給計數器增長delta,好比啓動1個協程就增長1。
  2. Done():協程退出前執行,把計數器減1。
  3. Wait():阻塞等待計數器爲0。

考一考

下面的程序是建立了協程father,而後father協程建立了10個子協程,main函數等待全部協程結束後退出,看看下面代碼有沒有什麼問題?數據結構

package main

import (
    "fmt"
    "sync"
)

func father(wg *sync.WaitGroup) {
    wg.Add(1)
    defer wg.Done()

    fmt.Printf("father\n")
    for i := 0; i < 10; i++ {
        go child(wg, i)
    }
}

func child(wg *sync.WaitGroup, id int) {
    wg.Add(1)
    defer wg.Done()

    fmt.Printf("child [%d]\n", id)
}

func main() {
    var wg sync.WaitGroup
    go father(&wg)

    wg.Wait()
    log.Printf("main: father and all chindren exit")
}

發現問題了嗎?若是沒有看下面的運行結果:main函數在子協程結束前就開始結束了。併發

father
main: father and all chindren exit
child [9]
child [0]
child [4]
child [7]
child [8]

陷阱分析

產生以上問題的緣由在於,建立協程後在協程內才執行Add()函數,而此時Wait()函數可能已經在執行,甚至Wait()函數在全部Add()執行前就執行了,Wait()執行時立馬就知足了WaitGroup的計數器爲0,Wait結束,主程序退出,致使全部子協程還沒徹底退出,main函數就結束了。app

正確的作法

Add函數必定要在Wait函數執行前執行,這在Add函數的文檔中就提示了: Note that calls with a positive delta that occur when the counter is zero must happen before a Wait.函數

如何確保Add函數必定在Wait函數前執行呢?在協程狀況下,咱們不能預知協程中代碼執行的時間是否早於Wait函數的執行時間,可是,咱們能夠在分配協程前就執行Add函數,而後再執行Wait函數,以此確保。code

下面是修改後的程序,以及輸出結果。協程

package main

import (
    "fmt"
    "sync"
)

func father(wg *sync.WaitGroup) {
    defer wg.Done()

    fmt.Printf("father\n")
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go child(wg, i)
    }
}

func child(wg *sync.WaitGroup, id int) {
    defer wg.Done()
    fmt.Printf("child [%d]\n", id)
}

func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    go father(&wg)

    wg.Wait()
    fmt.Println("main: father and all chindren exit")
}
father
child [9]
child [7]
child [8]
child [1]
child [4]
child [5]
child [2]
child [6]
child [0]
child [3]
main: father and all chindren exit
若是這篇文章對你有幫助,請點個贊/喜歡,讓我知道個人寫做是有價值的,感謝。
相關文章
相關標籤/搜索