sync.WaitGroup
是並發環境中,相當常用的資料結構,用來等待所有協程的結束,寫程式碼的時候都是照著例子的樣子寫的,也沒用深究過它的使用。前幾日想著能不能在協程中執行Add()
函數,答案是不能,這裡介紹下。
陷阱在WaitGroup的3個函數的呼叫順序上。先回顧下3個函數的功能:
-
Add(delta int)
:增加計數器delta,例如啟動1個協程就增加1。 -
Done()
:協程退出前執行,把計數器減1。 -
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函數就結束了。
正確的做法
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函數,以此確保。
以下是修改後的程序,以及輸出結果。
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