go的WaitGroup使用及源碼分析

源碼使用的是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
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章