這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
0x01 介紹
經常會看到以下了代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
package main
import ( "fmt" "time" )
func main(){ for i := 0; i < 100 ; i++{ go fmt.Println(i) } time.Sleep(time.Second) }
|
主線程為了等待goroutine都運行完畢,不得不在程式的末尾使用time.Sleep()
來睡眠一段時間,等待其他線程充分運行。對於簡單的代碼,100個for迴圈可以在1秒之內運行完畢,time.Sleep()
也可以達到想要的效果。
但是對於實際生活的大多數情境來說,1秒是不夠的,並且大部分時候我們都無法預知for迴圈內代碼已耗用時間的長短。這時候就不能使用time.Sleep()
來完成等待操作了。
可以考慮使用管道來完成上述操作:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
func main() { c := make(chan bool, 100) for i := 0; i < 100; i++ { go func(i int) { fmt.Println(i) c <- true }(i) }
for i := 0; i < 100; i++ { <-c } }
|
首先可以肯定的是使用管道是能達到我們的目的的,而且不但能達到目的,還能十分完美的達到目的。
但是管道在這裡顯得有些大材小用,因為它被設計出來不僅僅只是在這裡用作簡單的同步處理,在這裡使用管道實際上是不合適的。而且假設我們有一萬、十萬甚至更多的for迴圈,也要申請同樣數量大小的管道出來,對記憶體也是不小的開銷。
對於這種情況,go語言中有一個其他的工具sync.WaitGroup
能更加方便的協助我們達到這個目的。
WaitGroup
對象內部有一個計數器,最初從0開始,它有三個方法:Add(), Done(), Wait()
用來控制計數器的數量。Add(n)
把計數器設定為n
,Done()
每次把計數器-1
,wait()
會阻塞代碼的運行,直到計數器地值減為0。
使用WaitGroup
將上述代碼可以修改為:
1 2 3 4 5 6 7 8 9 10 11
|
func main() { wg := sync.WaitGroup{} wg.Add(100) for i := 0; i < 100; i++ { go func(i int) { fmt.Println(i) wg.Done() }(i) } wg.Wait() }
|
這裡首先把wg
計數設定為100, 每個for迴圈運行完畢都把計數器減一,主函數中使用Wait()
一直阻塞,直到wg為零——也就是所有的100個for迴圈都運行完畢。相對於使用管道來說,WaitGroup
輕巧了許多。
0x02 注意事項
1. 計數器不能為負值
我們不能使用Add()
給wg
設定一個負值,否則代碼將會報錯:
1 2 3 4 5 6 7
|
panic: sync: negative WaitGroup counter
goroutine 1 [running]: sync.(*WaitGroup).Add(0xc042008230, 0xffffffffffffff9c) D:/Go/src/sync/waitgroup.go:75 +0x1d0 main.main() D:/code/go/src/test-src/2-Package/sync/waitgroup/main.go:10 +0x54
|
同樣使用Done()
也要特別注意不要把計數器設定成負數了。
2. WaitGroup對象不是一個參考型別
WaitGroup對象不是一個參考型別,在通過函數傳值的時候需要使用地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
func main() { wg := sync.WaitGroup{} wg.Add(100) for i := 0; i < 100; i++ { go f(i, &wg) } wg.Wait() }
// 一定要通過指標傳值,不然進程會進入死結狀態 func f(i int, wg *sync.WaitGroup) { fmt.Println(i) wg.Done() }
|