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/ ),不得用於商業目的,基於本文修改后的作品務必以相同的許可發布。