源碼使用的是1.9版本;sync 包裏的WaitGroup主要用於線程的同步;計數主線程創建的子線程(WaitGoup.Add(i));調用清除標記方法(WaitGroup.Done());使用WaitGroup.Wait()來阻塞,直到所有子線程(標記=0)執行完畢。
例子:
package main
import (
"sync"
"fmt"
)
func main(){
var swg sync.WaitGroup
for i:=0;i<3;i++{
//增加一個計數器
swg.Add(1)
go func(wg *sync.WaitGroup,mark int){
//減去計數器
defer wg.Done()//等價於 wg.Add(-1)
fmt.Printf("%d goroutine finish \n",mark)
}(&swg,i)
}
//等待所有go程結束
swg.Wait()
}
結果:
2 goroutine finish
1 goroutine finish
0 goroutine finish
注意!如果將代碼改成下面這樣(子線程函數,傳入的參數是waitgroup的值拷貝),會出現什麼情況呢?
func main(){
var swg sync.WaitGroup
for i:=0;i<3;i++{
swg.Add(1)
go func(wg sync.WaitGroup,mark int){
defer wg.Done()
fmt.Printf("%d goroutine finish \n",mark)
}(swg,i)
}
swg.Wait()
}
結果:
2 goroutine finish
fatal error: all goroutines are asleep - deadlock!
1 goroutine finish
0 goroutine finish
goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc0420080dc)
C:/Go/src/runtime/sema.go:56 +0x40
sync.(*WaitGroup).Wait(0xc0420080d0)
C:/Go/src/sync/waitgroup.go:131 +0x79
出現死鎖,因爲子協程傳入的waitGroup對象是一份新值拷貝,主協程的waitGroup並沒有調用Done()方法,導致標誌位無法被釋放;各位童鞋在使用的時候,記得傳入waitGroup的引用拷貝。
WaitGroup源碼分析(精簡了無關主要邏輯的代碼)
1、首先查看WaitGroup的數據結構:
type WaitGroup struct {
noCopy noCopy
//共12個字節,低4字節用於記錄wait等待次數,高8字節是計數器(64位機器是高8字節,32機器是中間4個字節,因爲64位機器的原子操作需要64位的對齊,但是32位的編譯器不能確保。)
state1 [12]byte
//用於喚醒go程的信號量
sema uint32
}
2、WaitGroup.Add()方法
func (wg *WaitGroup) Add(delta int) {
statep := wg.state()
//將標記爲加delta
state := atomic.AddUint64(statep, uint64(delta)<<32)
//獲得計數器數值
v := int32(state >> 32)
//獲得wait()等待次數
w := uint32(state)
//標記位不能小於0(done過多或者Add()負值太多)
if v < 0 {
panic("sync: negative WaitGroup counter")
}
//不能併發的Add() 和Done()
if w != 0 && delta > 0 && v == int32(delta) {
panic("sync: WaitGroup misuse: Add called concurrently with Wait")
}
//Add 完畢
if v > 0 || w == 0 {
return
}
//執行到這,此時計數器V=0;那麼等待計數器肯定和整個state的值相等,不然只有一個情況:有人調了Add(),並且是併發調用的。
if *statep != state {
panic("sync: WaitGroup misuse: Add called concurrently with Wait")
}
//所有狀態位清零
*statep = 0
//喚醒等待的go程
for ; w != 0; w-- {
runtime_Semrelease(&wg.sema, false)
}
}
//根據編譯器位數,獲得標誌位和等待次數的數據域
func (wg *WaitGroup) state() *uint64 {
if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 {
return (*uint64)(unsafe.Pointer(&wg.state1))
} else {
return (*uint64)(unsafe.Pointer(&wg.state1[4]))
}
}
// Done方法其實就是Add(-1)
func (wg *WaitGroup) Done() {
wg.Add(-1)
}
3、Wait方法
func (wg *WaitGroup) Wait() {
statep := wg.state()
//循環檢查計數器V啥時候等於0
for {
state := atomic.LoadUint64(statep)
v := int32(state >> 32)
w := uint32(state)
//v==0說明go程執行結束
if v == 0 {
return
}
//尚有未執行完的go程,等待標誌位+1(直接在低位處理,無需移位)
if atomic.CompareAndSwapUint64(statep, state, state+1) {
runtime_Semacquire(&wg.sema)
if *statep != 0 {
panic("sync: WaitGroup is reused before previous Wait has returned")
}
return
}
}
}