319
技術社區[雲棲]
《GO並發編程實戰》—— WaitGroup
我們在第6章多次提到過sync.WaitGroup類型和它的方法。sync.WaitGroup類型的值也是開箱即用的。例如,在聲明
之後,我們就可以直接正常使用wg變量了。該類型有三個指針方法,即Add、Done和Wait。
類型sync.WaitGroup是一個結構體類型。在它之中有一個代表計數的字段。當一個sync.WaitGroup類型的變量被聲明之後,其值中的那個計數值將會是0。我們可以通過該值的Add方法增大或減少其中的計數值。例如:
或
雖然Add方法接受一個int類型的值,並且我們也可以通過該方法減少計數值,但是我們一定不要讓計數值變為負數。因為這樣會立即引發一個運行恐慌。這也代表著我們對sync.WaitGroup類型值的錯誤使用。
除了調用sync.WaitGroup類型值的Add方法並傳入一個負數之外,我們還可以通過調用該值的Done來使其中的計數值減一。也就是說,下麵這三條語句與wg.Add(-3)的執行效果是一致的:
使用該方法禁忌與Add方法的一樣——不要讓相應的計數值變為負數。例如,這段代碼中的第5條語句會引發一個運行時恐慌:
我們現在知道,使用sync.WaitGroup類型值的Add方法和Done方法可以變更其中的計數值。那麼變更這個計數值有什麼用呢?
當我們調用sync.WaitGroup類型值的Wait方法的時候,它會去檢查該值中的計數值。如果這個計數值為0,那麼該方法會立即返回,且不會對程序的運行產生任何影響。 但是,如果這個計數值大於0,那麼該方法的調用方所屬的那個Goroutine就會被阻塞。直到該計數值重新變為0之時,為此而被阻塞的所有Goroutine才會被喚醒。
這個類型的值一般被用來協調多個Goroutine的運行。假設,在我們的程序中啟用了4個Goroutine,分別是G1、G2、G3和G4。其中,G2、G3和G4是由G1中的代碼啟用並被用於執行某些特定任務的。G1在啟用這3個Goroutine之後要等待這些特定任務的完成。在這種情況下,我們有兩個方案。
第一個方案是使用前文講到的通道來傳遞任務完成信號。例如,我們在啟用G2、G3和G4之前聲明這樣一個通道:
1 |
sign := make(chan byte , 3 )
|
然後,在G2、G3和G4執行的任務完成之後立即向該通道發送代表了某個任務已被執行完成的元素值:
最後,在啟用這幾個Goroutine之後,我們還要在G1執行的函數中添加類似這樣的代碼以等待相關的任務完成信號:
1 |
for i := 0 ; i < 3 ; i++ {
|
2 |
fmt.Printf( "G%d is ended.\n" , <-sign)
|
// 省略若幹條語句
這樣的方法固然是有效的。上麵的這條for語句會等到G2、G3和G4都被運行結束之後才會被執行結束,繼而其後麵的語句才會得以執行。sign通道起到了協調這4個Goroutine的運行的作用。
不過,對於這樣一個簡單的協調工作來說,使用通道是否過重了?或者說,通道sign是否被大材小用了?通道的實現中包含了很多專為並發安全的數據而建立的數據結構和算法。原則上說,我們不應該把通道當做互斥鎖或信號燈來說用。在這裏使用它並沒有體現出它的優勢,反而會在代碼易讀性和程序性能方麵打一些折扣。
該需求的第二個方案就是使用sync.WaitGroup類型值。對應的代碼如下:
20 |
fmt.Println( "G2, G3 and G4 are ended." )
|
可以看到,我們在啟用G2、G3和G4之前先聲明了一個sync.WaitGroup類型的變量wg,並調用其值的Add方法以使其中的計數值等於將要額外啟用的Goroutine的個數。然後,在G2、G3和G4的運行即將結束之前,我們分別通過調用wg.Done方法將其中的計數值減去1。最後,我們在G1中調用wg.Wait方法以等待G2、G3和G4中的那3個對wg.Done方法的調用的完成。待這3個調用完成之時,在wg.Wait()處被阻塞的G1會被喚醒,它後麵的那條語句也會被立即執行。
不論是Add方法還是Done方法,它們在被執行的時候都會在增大或減小其所屬值中的那個計數值之後對它進行判斷。如果該計數值為0,那麼該方法就會喚醒所有已為此而被阻塞的Goroutine(如果有的話)。這些Goroutine即是在從該計數值最近一次變為正整數到此時(即重新變為0)的時間段內執行了同一個sync.WaitGroup類型值的Wait方法的Goroutine。
顯然,我們的第二個方案更加適合這裏的應用場景。它在代碼的清晰度和性能損耗方麵都會更勝一籌。
在這裏,我們可以總結出一些使用一個sync.WaitGroup類型值的方法和規則。
• 對一個sync.WaitGroup類型值的Add方法的的第一次調用應該發生在對該值的Done方法進行調用之前。因為如果先調用了Done方法,那麼就會使該值中的計數值小於0,繼而引發運行時恐慌。由於這兩個方法通常不會在同一個Goroutine中被調用,所以調用Add方法的時機還應該提前到將會調用該值的Done方法的那個或那些Goroutine被啟用之前。
• 對一個sync.WaitGroup類型值的Add方法的第一次調用同樣應該發生在對該值的Wait方法進行調用之前。如果在我們調用Wait方法的時候該值的計數值等於0,那麼該方法將會直接返回而不會阻塞調用方所屬的Goroutine。這往往是與我們的期望相反的。
• 在一個sync.WaitGroup類型值的生命周期內,其中的計數值總是由起初的0變為某個正整數(或先後變為某幾個正整數),然後再回歸為0。我們把完成這樣一個變化曲線所用的時間稱為一個計數周期。關於此的一個示意如圖8-1所示。

如圖所示,計數值的每次變化都是由對其所屬值的Add方法或Done方法的調用引起的。一個計數周期總是從對其所屬值的Add方法的調用開始的,並且也總是以對其所屬值的Add方法或Done方法的調用為結束標誌的。我們若在一個計數周期之內(不包含計數值等於0的兩端)調用其所屬值的Wait方法則會使調用方所在的Goroutine被阻塞,直至該計數周期結束的那一刻。
• sync.WaitGroup類型值是可以被複用的。也就是說,此類值的生命周期可以包含任意個計數周期。一旦一個計數周期結束,我們在前麵對該值的那些方法的調用所產生的作用也將消失。也就是說,它們不會影響到後續計數周期中的該值的計數值以及參與改變該計數值的各方。換句話講,一個sync.WaitGroup類型值在其每個計數周期中的狀態和作用都是獨立的。
最後,值得說明的是,在sync.WaitGroup類型及其方法中也用到了在前麵章節中提到的互斥鎖、原子操作和信號燈機製。這使得我們總是可以在任意個Goroutine中並發的調用同一個sync.WaitGroup類型值的那些方法。也就是說,它們都是並發安全的。
本節所講的sync.WaitGroup類型提供了一種方式,使我們可以對多個Goroutine的運行進行簡單的協調。這得益於它提供的那幾個以計數值為基礎的易用方法,以及它的並發安全特性。隻要理解了每個方法對計數值的操縱方式以及意義,我們就可以用好該類型的值了。我們剛剛說明的那些使用方法和規則對理解該類型及其方法應該是非常有幫助的。
最後更新:2017-05-23 12:02:39