Go語言核心36講(Go語言實戰與應用五)--學習筆記


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。

image

(互斥鎖與條件變量)

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM