Go - sync.Cond

設(shè)計(jì)目的

為等待 / 通知場(chǎng)景下的并發(fā)問題提供支持。Cond 通常應(yīng)用于等待某個(gè)條件的一組 goroutine,等條件變?yōu)?true 的時(shí)候,其中一個(gè) goroutine 或者所有的 goroutine 都會(huì)被喚醒執(zhí)行。

顧名思義,Cond 是和某個(gè)條件相關(guān),這個(gè)條件需要一組 goroutine 協(xié)作共同完成,在條件還沒有滿足的時(shí)候,所有等待這個(gè)條件的 goroutine 都會(huì)被阻塞住,只有這一組 goroutine 通過協(xié)作達(dá)到了這個(gè)條件,等待的 goroutine 才可能繼續(xù)進(jìn)行下去。

基本用法

標(biāo)準(zhǔn)庫中的 Cond 并發(fā)原語初始化的時(shí)候,需要關(guān)聯(lián)一個(gè) Locker 接口的實(shí)例,一般我們使用 Mutex 或者 RWMutex。


type Cond func NeWCond(l Locker) *Cond

func (c *Cond) Broadcast()
func (c *Cond) Signal()
func (c *Cond) Wait()

Cond 關(guān)聯(lián)的 Locker 實(shí)例可以通過 c.L 訪問,它內(nèi)部維護(hù)著一個(gè)先入先出的等待隊(duì)列。

  • Signal 方法,允許調(diào)用者 Caller 喚醒一個(gè)等待此 Cond 的 goroutine。如果此時(shí)沒有等待的 goroutine,顯然無需通知 waiter;如果 Cond 等待隊(duì)列中有一個(gè)或者多個(gè)等待的 goroutine,則需要從等待隊(duì)列中移除第一個(gè) goroutine 并把它喚醒。 調(diào)用 Signal 方法時(shí),不強(qiáng)求一定要持有 c.L 的鎖。

  • Broadcast 方法,允許調(diào)用者 Caller 喚醒所有等待此 Cond 的 goroutine。如果此時(shí)沒有等待的 goroutine,顯然無需通知 waiter;如果 Cond 等待隊(duì)列中有一個(gè)或者多個(gè)等待的 goroutine,則清空所有等待的 goroutine,并全部喚醒。 調(diào)用 Broadcast 方法時(shí),也不強(qiáng)求一定持有 c.L 的鎖。

  • Wait 方法,會(huì)把調(diào)用者 Caller 放入 Cond 的等待隊(duì)列中并阻塞,直到被 Signal 或者 Broadcast 的方法從等待隊(duì)列中移除并喚醒。調(diào)用 Wait 方法時(shí)必須要持有 c.L 的鎖。

例子


func main() {
    c := sync.NewCond(&sync.Mutex{})
    var ready int

    for i := 0; i < 10; i++ {
        go func(i int) {
            time.Sleep(time.Duration(rand.Int63n(10)) * time.Second)

            // 加鎖更改等待條件
            c.L.Lock()
            ready++
            c.L.Unlock()

            log.Printf("運(yùn)動(dòng)員#%d 已準(zhǔn)備就緒\n", i)
            // 廣播喚醒所有的等待者
            c.Broadcast()
        }(i)
    }

    c.L.Lock()
    for ready != 10 {
        c.Wait()
        log.Println("裁判員被喚醒一次")
    }
    c.L.Unlock()

    //所有的運(yùn)動(dòng)員是否就緒
    log.Println("所有運(yùn)動(dòng)員都準(zhǔn)備就緒。比賽開始,3,2,1, ......")
}

它的復(fù)雜在于:一,這段代碼有時(shí)候需要加鎖,有時(shí)候可以不加;二,Wait 喚醒后需要檢查條件;三,條件變量的更改,其實(shí)是需要原子操作或者互斥鎖保護(hù)的。

實(shí)現(xiàn)原理

type Cond struct {
    noCopy noCopy

    // 當(dāng)觀察或者修改等待條件的時(shí)候需要加鎖
    L Locker

    // 等待隊(duì)列, 一個(gè) Goroutine 的鏈表,它是實(shí)現(xiàn)同步機(jī)制的核心結(jié)構(gòu)
    notify  notifyList
    checker copyChecker
}

func NewCond(l Locker) *Cond {
    return &Cond{L: l}
}

func (c *Cond) Wait() {
    c.checker.check()
    // 增加到等待隊(duì)列中
    t := runtime_notifyListAdd(&c.notify)
    c.L.Unlock()
    // 阻塞休眠直到被喚醒
    runtime_notifyListWait(&c.notify, t)
    c.L.Lock()
}

func (c *Cond) Signal() {
    c.checker.check()
    runtime_notifyListNotifyOne(&c.notify)
}

func (c *Cond) Broadcast() {
    c.checker.check()
    runtime_notifyListNotifyAll(&c.notify)
}

type notifyList struct {
    wait uint32
    notify uint32

    lock mutex
    head *sudog
    tail *sudog
}

head 和 tail 分別指向的鏈表的頭和尾,wait 和 notify 分別表示當(dāng)前正在等待的和已經(jīng)通知到的 Goroutine 的索引.

Cond 的使用

Cond 在實(shí)際項(xiàng)目中被使用的機(jī)會(huì)比較少,原因總結(jié)起來有兩個(gè)。

第一,同樣的場(chǎng)景我們會(huì)使用其他的并發(fā)原語來替代。Go 特有的 Channel 類型,有一個(gè)應(yīng)用很廣泛的模式就是通知機(jī)制,這個(gè)模式使用起來也特別簡(jiǎn)單。所以很多情況下,我們會(huì)使用 Channel 而不是 Cond 實(shí)現(xiàn) wait/notify 機(jī)制。

第二,對(duì)于簡(jiǎn)單的 wait/notify 場(chǎng)景,比如等待一組 goroutine 完成之后繼續(xù)執(zhí)行余下的代碼,我們會(huì)使用 WaitGroup 來實(shí)現(xiàn)。因?yàn)?WaitGroup 的使用方法更簡(jiǎn)單,而且不容易出錯(cuò)。比如,上面百米賽跑的問題,就可以很方便地使用 WaitGroup 來實(shí)現(xiàn)。

總結(jié)

  • sync.Cond.Wait 在調(diào)用之前一定要使用獲取互斥鎖,否則會(huì)觸發(fā)程序崩潰.
  • sync.Cond.Signal 喚醒的 Goroutine 都是隊(duì)列最前面、等待最久的 Goroutine.
  • sync.Cond.Broadcast 會(huì)按照一定順序廣播通知等待的全部 Goroutine.
  • waiter goroutine 被喚醒不等于等待條件被滿足
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容