1. 程式人生 > >條件變數 sync.Cond

條件變數 sync.Cond

sync.Cond 條件變數是基於互斥鎖的,它必須有互斥鎖的支撐才能發揮作用。

  • sync.Cond 條件變數用來協調想要訪問共享資源的那些執行緒,當共享資源的狀態發生變化的時候,它可以用來通知被互斥鎖阻塞的執行緒
  • 條件變數的初始化離不開互斥鎖,並且它的方法也是基於互斥鎖的
  • 條件變數有三個方法,等待通知(wait),單發通知(signal),廣播通知(broadcast)。當互斥鎖鎖定時,可以進行等待通知;當互斥鎖解鎖時,可以進行單發通知和廣播通知。
var mailbox uint8
var lock sync.RWMutex
sendCond := sync.NewCond(&lock)
recvCond := sync.NewCond(lock.RLocker())

有幾個點需要知道

  • sync.Cond 通過sync.NewCond(sync.Locker)初始化,初始化函式需要一個sync.Locker的引數值
  • sync.Locker其實是一個介面,包含Lock()和Unlock()方法。sync.Mutex和sync.RWMutex都有Lock和Unlock方法,只不過它們都是指標方法。因此,這兩個型別的指標型別才是sync.Locker介面的實現型別
  • 通過lock.RLock()獲得讀鎖,這個讀鎖能呼叫lock變數的RLock和RUnlock方法,實現對讀鎖的解鎖和鎖定。

生產者,這裡看作向mailbox產生值的物件

lock.Lock()
for mailbox == 1 {
    sendCond.Wait()
}
mailbox = 1
lock.Unlock()
recvCond.Signal()

消費者,向mailbox取值的物件

lock.RLock()
for mailbox == 0 {
 recvCond.Wait()
}
mailbox = 0
lock.RUnlock()
sendCond.Signal()

條件變數的Wait方法主要做了四件事。

  • 把呼叫它的 goroutine(也就是當前的 goroutine)加入到當前條件變數的通知佇列中。
  • 解鎖當前的條件變數基於的那個互斥鎖。
  • 讓當前的 goroutine 處於等待狀態,等到通知到來時再決定是否喚醒它。此時,這個 goroutine 就會阻塞在呼叫這個Wait方法的那行程式碼上。
  • 如果通知到來並且決定喚醒這個 goroutine,那麼就在喚醒它之後重新鎖定當前條件變數基於的互斥鎖。自此之後,當前的 goroutine 就會繼續執行後面的程式碼了。

if語句只會對共享資源的狀態檢查一次,而for語句卻可以做多次檢查,直到這個狀態改變為止。那為什麼要做多次檢查呢?這主要是為了保險起見。如果一個 goroutine 因收到通知而被喚醒,但卻發現共享資源的狀態,依然不符合它的要求,那麼就應該再次呼叫條件變數的Wait方法,並繼續等待下次通知的到來。

在 Go 語言中,我們需要用sync.NewCond函式來初始化一個sync.Cond型別的條件變數。

sync.NewCond函式需要一個sync.Locker型別的引數值。

sync.Mutex型別的值以及sync.RWMutex型別的值都可以滿足這個要求。都可以滿足這個要求。另外,後者的RLocker方法可以返回這個值中的讀鎖,也同樣可以作為sync.NewCond函式的引數值,如此就可以生成與讀寫鎖中的讀鎖對應的條件變量了。

條件變數的Wait方法需要在它基於的互斥鎖保護下執行,否則就會引發不可恢復的 panic。此外,我們最好使用for語句來檢查共享資源的狀態,幷包裹對條件變數的Wait方法的呼叫。

不要用if語句,因為它不能重複地執行”檢查狀態 - 等待通知 - 被喚醒“的這個流程。重複執行這個流程的原因是,一個因等待通知,而被阻塞的 goroutine,可能會在共享資源的狀態不滿足其要求的情況下被喚醒。

條件變數的Signal方法只會喚醒一個因等待通知而被阻塞的 goroutine,而它的Broadcast方法卻可以喚醒所有為此而等待的 goroutine。後者比前者的適應場景要多得多。

這兩個方法並不需要受到互斥鎖的保護,我們也最好不要在解鎖互斥鎖之前呼叫它們。還有,條件變數的通知具有即時性。當通知被髮送的時候,如果沒有任何 goroutine 需要被喚醒,那麼該通知就會立即失效。