27 | 條件變數sync.Cond (上)
前導內容:條件變數與互斥鎖
我們常常會把條件變數這個同步工具拿來與互斥鎖一起討論。實際上,條件變數是基於互斥鎖的,它必須有互斥鎖的支撐才能發揮作用。
條件變數並不是被用來保護臨界區和共享資源的,它是用於協調想要訪問共享資源的那些執行緒的。當共享資源的狀態發生變化時,它可以被用來通知被互斥鎖阻塞的執行緒。
比如說,我們兩個人在共同執行一項祕密任務,這需要在不直接聯絡和見面的前提下進行。我需要向一個信箱裡放置情報,你需要從這個信箱中獲取情報。這個信箱就相當於一個共享資源,而我們就分別是進行寫操作的執行緒和進行讀操作的執行緒。
如果我在放置的時候發現信箱裡還有未被取走的情報,那就不再放置,而先返回。另一方面,如果你在獲取的時候發現信箱裡沒有情報,那也只能先回去了。這就相當於寫的執行緒或讀的執行緒阻塞的情況。
雖然我們倆都有信箱的鑰匙,但是同一時刻只能有一個人插入鑰匙並開啟信箱,這就是鎖的作用了。更何況我們們倆是不能直接見面的,所以這個信箱本身就可以被視為一個臨界區。
儘管沒有協調好,我們們倆仍然要想方設法的完成任務啊。所以,如果信箱裡有情報,而你卻遲遲未取走,那我就需要每過一段時間帶著新情報去檢查一次,若發現信箱空了,我就需要及時地把新情報放到裡面。
另一方面,如果信箱裡一直沒有情報,那你也要每過一段時間去開啟看看,一旦有了情報就及時地取走。這麼做是可以的,但就是太危險了,很容易被敵人發現。
後來,我們又想了一個計策,各自僱傭了一個不起眼的小孩兒。如果早上七點有一個戴紅色帽子的小孩兒從你家樓下路過,那麼就意味著信箱裡有了新情報。另一邊,如果上午九點有一個戴藍色帽子的小孩兒從我家樓下路過,那就說明你已經從信箱中取走了情報。
這樣一來,我們們執行任務的隱蔽性高多了,並且效率的提升非常顯著。這兩個戴不同顏色帽子的小孩兒就相當於條件變數,在共享資源的狀態產生變化的時候,起到了通知的作用。
當然了,我們是在用 Go 語言編寫程式,而不是在執行什麼祕密任務。因此,條件變數在這裡的最大優勢就是在效率方面的提升。當共享資源的狀態不滿足條件的時候,想操作它的執行緒再也不用迴圈往復地做檢查了,只要等待通知就好了。
說到這裡,想考考你知道怎麼使用條件變數嗎?所以,我們今天的問題就是:條件變數怎樣與互斥鎖配合使用?
這道題的典型回答是:條件變數的初始化離不開互斥鎖,並且它的方法有的也是基於互斥鎖的。
條件變數提供的方法有三個:等待通知(wait)、單發通知(signal)和廣播通知(broadcast)。
我們在利用條件變數等待通知的時候,需要在它基於的那個互斥鎖保護下進行。而在進行單發通知或廣播通知的時候,卻是恰恰相反的,也就是說,需要在對應的互斥鎖解鎖之後再做這兩種操作。
問題解析
問題解析這個問題看起來很簡單,但其實可以基於它, 延伸出很多其他的問題。比如,每個方法的使用時機是什麼?又比如,每個方法執行的內部流程是怎樣的?
下面,我們一邊用程式碼實現前面那個例子,一邊討論條件變數的使用。
首先,我們先來建立如下幾個變數。
var mailbox uint8
var lock sync.RWMutex
sendCond := sync.NewCond(&lock)
recvCond := sync.NewCond(lock.RLocker())
變數mailbox代表信箱,是uint8型別的。 若它的值為0則表示信箱中沒有情報,而當它的值為1時則說明信箱中有情報。lock是一個型別為sync.RWMutex的變數,是一個讀寫鎖,也可以被視為信箱上的
那把鎖。
另外,基於這把鎖,我還建立了兩個代表條件變數的變數,名字分別叫sendCond和recvCond。 它們都是*sync.Cond型別的,同時也都是由sync.NewCond函式來初始化的。
與sync.Mutex型別和sync.RWMutex型別不同,sync.Cond型別並不是開箱即用的。我們只能利用sync.NewCond函式建立它的指標值。這個函式需要一個sync.Locker型別的引數值。
還記得嗎?我在前面說過,條件變數是基於互斥鎖的,它必須有互斥鎖的支撐才能夠起作用。因此,這裡的引數值是不可或缺的,它會參與到條件變數的方法實現當中。
sync.Locker其實是一個介面,在它的宣告中只包含了兩個方法定義,即:Lock()和Unlock()。sync.Mutex型別和sync.RWMutex型別都擁有Lock方法和Unlock方法,只不過它們都是指標方法。因此,這兩個型別的指標型別才是sync.Locker介面的實現型別。
我在為sendCond變數做初始化的時候,把基於lock變數的指標值傳給了sync.NewCond函式。
原因是,lock變數的Lock方法和Unlock方法分別用於對其中寫鎖的鎖定和解鎖,它們與sendCond變數的含義是對應的。sendCond是專門為放置情報而準備的條件變數,向信箱裡放置情報,可以被視為對共享資源的寫操作。
相應的,recvCond變數代表的是專門為獲取情報而準備的條件變數。 雖然獲取情報也會涉及對信箱狀態的改變,但是好在做這件事的人只會有你一個,而且我們也需要藉此瞭解一下,條件變數與讀寫鎖中的讀鎖的聯用方式。所以,在這裡,我們暫且把獲取情報看做是對共享資源的讀操作。
因此,為了初始化recvCond這個條件變數,我們需要的是lock變數中的讀鎖,並且還需要是sync.Locker型別的。
可是,lock變數中用於對讀鎖進行鎖定和解鎖的方法卻是RLock和RUnlock,它們與sync.Locker介面中定義的方法並不匹配。
好在sync.RWMutex型別的RLocker方法可以實現這一需求。我們只要在呼叫sync.NewCond函式時,傳入呼叫表示式lock.RLocker()的結果值,就可以使該函式返回符合要求的條件變數了。
為什麼說通過lock.RLocker()得來的值就是lock變數中的讀鎖呢?實際上,這個值所擁有的Lock方法和Unlock方法,在其內部會分別呼叫lock變數的RLock方法和RUnlock方法。也就是說,前兩個方法僅僅是後兩個方法的代理而已。
好了,我們現在有四個變數。一個是代表信箱的mailbox,一個是代表信箱上的鎖的lock。還有兩個是,代表了藍帽子小孩兒的sendCond,以及代表了紅帽子小孩兒的recvCond。
(互斥鎖與條件變數)
我,現在是一個 goroutine(攜帶的go函式),想要適時地向信箱裡放置情報並通知你,應該怎麼做呢?
lock.Lock()
for mailbox == 1 {
sendCond.Wait()
}
mailbox = 1
lock.Unlock()
recvCond.Signal()
我肯定需要先呼叫lock變數的Lock方法。注意,這個Lock方法在這裡意味的是:持有信箱上的鎖,並且有開啟信箱的權利,而不是鎖上這個鎖。
然後,我要檢查mailbox變數的值是否等於1,也就是說,要看看信箱裡是不是還存有情報。如果還有情報,那麼我就回家去等藍帽子小孩兒了。
這就是那條for語句以及其中的呼叫表示式sendCond.Wait()所表示的含義了。你可能會問,為什麼這裡是for語句而不是if語句呢?我在後面會對此進行解釋的。
我們再往後看,如果信箱裡沒有情報,那麼我就把新情報放進去,關上信箱、鎖上鎖,然後離開。用程式碼表達出來就是mailbox = 1和lock.Unlock()。
離開之後我還要做一件事,那就是讓紅帽子小孩兒準時去你家樓下路過。也就是說,我會及時地通知你“信箱裡已經有新情報了”,我們呼叫recvCond的Signal方法就可以實現這一步驟。
另一方面,你現在是另一個 goroutine,想要適時地從信箱中獲取情報,然後通知我。
lock.RLock()
for mailbox == 0 {
recvCond.Wait()
}
mailbox = 0
lock.RUnlock()
sendCond.Signal()
你跟我做的事情在流程上其實基本一致,只不過每一步操作的物件是不同的。你需要呼叫的是lock變數的RLock方法。因為你要進行的是讀操作,並且會使用recvCond變數作為輔助。recvCond與lock變數的讀鎖是對應的。
在開啟信箱後,你要關注的是信箱裡是不是沒有情報,也就是檢查mailbox變數的值是否等於0。
如果它確實等於0,那麼你就需要回家去等紅帽子小孩兒,也就是呼叫recvCond的Wait方法。這裡使用的依然是for語句。如果信箱裡有情報,那麼你就應該取走情報,關上信箱、鎖上鎖,然後離開。對應的程式碼是mailbox = 0和lock.RUnlock()。之後,你還需要讓藍帽子小孩兒準時去我家樓下路過。這樣我就知道信箱中的情報已經被你獲取了。
以上這些,就是對我們們倆要執行祕密任務的程式碼實現。其中的條件變數的用法需要你特別注意。
再強調一下,只要條件不滿足,我就會通過呼叫sendCond變數的Wait方法,去等待你的通知,只有在收到通知之後我才會再次檢查信箱。
另外,當我需要通知你的時候,我會呼叫recvCond變數的Signal方法。你使用這兩個條件變數的方式正好與我相反。你可能也看出來了,利用條件變數可以實現單向的通知,而雙向的通知則需要兩個條件變數。這也是條件變數的基本使用規則。
看到上述例子的全部實現程式碼
package main
import (
"log"
"sync"
"time"
)
func main() {
// mailbox 代表信箱。
// 0代表信箱是空的,1代表信箱是滿的。
var mailbox uint8
// lock 代表信箱上的鎖。
var lock sync.RWMutex
// sendCond 代表專用於發信的條件變數。
sendCond := sync.NewCond(&lock)
// recvCond 代表專用於收信的條件變數。
recvCond := sync.NewCond(lock.RLocker())
// sign 用於傳遞演示完成的訊號。
sign := make(chan struct{}, 3)
max := 5
go func(max int) { // 用於發信。
defer func() {
sign <- struct{}{}
}()
for i := 1; i <= max; i++ {
time.Sleep(time.Millisecond * 500)
lock.Lock()
for mailbox == 1 {
sendCond.Wait()
}
log.Printf("sender [%d]: the mailbox is empty.", i)
mailbox = 1
log.Printf("sender [%d]: the letter has been sent.", i)
lock.Unlock()
recvCond.Signal()
}
}(max)
go func(max int) { // 用於收信。
defer func() {
sign <- struct{}{}
}()
for j := 1; j <= max; j++ {
time.Sleep(time.Millisecond * 500)
lock.RLock()
for mailbox == 0 {
recvCond.Wait()
}
log.Printf("receiver [%d]: the mailbox is full.", j)
mailbox = 0
log.Printf("receiver [%d]: the letter has been received.", j)
lock.RUnlock()
sendCond.Signal()
}
}(max)
<-sign
<-sign
}
總結
我們這兩期的文章會圍繞條件變數的內容展開,條件變數是基於互斥鎖的一種同步工具,它必須有互斥鎖的支撐才能發揮作用。 條件變數可以協調那些想要訪問共享資源的執行緒。當共享資源的狀態發生變化時,它可以被用來通知被互斥鎖阻塞的執行緒。我在文章舉了一個兩人訪問信箱的例子,並用程式碼實現了這個過程。
思考題
*sync.Cond型別的值可以被傳遞嗎?那sync.Cond型別的值呢?
筆記原始碼
https://github.com/MingsonZheng/go-core-demo
本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。
歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含連結: http://www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。