GO中的條件變量sync.Cond詳解
GO的條件變量
一、條件變量與互斥鎖
- 條件變量是基于互斥鎖的,它必須基于互斥鎖才能發(fā)揮作用;
- 條件變量并不是用來保護(hù)臨界區(qū)和共享資源的,它是用來協(xié)調(diào)想要訪問共享資源的那些線程的;
- 在Go語言中,條件變量最大的優(yōu)勢是效率方面的提升。當(dāng)共享資源不滿足條件的時(shí)候,想操作它的線程不用循環(huán)往返地檢查了,只要等待通知就好了。
二、條件變量與互斥鎖的配合使用
條件變量的初始化離不開互斥鎖,并且它的方法有點(diǎn)也是基于互斥鎖的。
條件變量提供的三個(gè)方法:等待通知(wait)、單發(fā)通知(signal)、廣發(fā)通知(broadcast)。
三、條件變量的使用
(1)創(chuàng)建鎖和條件
// mailbox 代表信箱 // 0 代表信箱是空的,1代表信箱是滿的 var mailbox uint8 // lock 代表信箱上的鎖 var lock sync.RWMutex // sendCond 代表專用于發(fā)信的條件變量 var sendCond = sync.NewCond(&lock) // reveCond 代表專用于收信的條件變量 var reveCond = sync.NewCond(lock.RLocker())
sync.Cond
類型并不是開箱即用的,只能利用sync.NewCond
創(chuàng)建它的指針值。這個(gè)函數(shù)需要sync.Locker
類型的參數(shù)值。sync.Locker
是一個(gè)接口,它包含兩個(gè)指針方法,即Lock()
和Unlock()
;因此,sync.Mutex
和sync.RWMutex
這兩個(gè)類型的指針類型才是sync.Locker
接口的實(shí)現(xiàn)類型。- 上面lock變量的Lock方法和Unlock方法分別用于對(duì)其中寫鎖的鎖定和解鎖,它們與sendCond變量的含義對(duì)應(yīng)。
lock.RLocker()
得到的值,擁有Lock和Unlock方法,其內(nèi)部會(huì)分別調(diào)用lock變量的RLock方法和RUnlock方法;
(2)使用
lock.Lock() for mailbox == 1 { sendCond.Wait() } mailbox = 1 lock.Unlock() recvCond.Signal()
lock.RLock() for mailbox == 0 { recvCond.Wait() } mailbox = 0 lock.RUnlock() sendCond.Signal()
完整代碼:
package main import ( "log" "sync" "time" ) func main() { // mailbox 代表信箱 // 0 代表信箱是空的,1代表信箱是滿的 var mailbox uint8 // lock 代表信箱上的鎖 var lock sync.RWMutex // sendCond 代表專用于發(fā)信的條件變量 var sendCond = sync.NewCond(&lock) // reveCond 代表專用于收信的條件變量 var reveCond = sync.NewCond(lock.RLocker()) // sign 用于傳遞演示完成的信號(hào) sign := make(chan struct{}, 2) max := 5 go func(max int) { // 用于發(fā)信 defer func() { sign <- struct{}{} }() for i := 1; i <= max; i++ { time.Sleep(time.Millisecond * 5) 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() reveCond.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 { reveCond.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 }
四、條件變量的Wait方法做了什么
(1)條件變量Wait方法主要做的四件事
條件變量的Wait方法主要做了四件事:
- 把調(diào)用它的goroutine(也就是當(dāng)前goroutine)加入到當(dāng)前條件變量的通知隊(duì)列中;
- 解鎖當(dāng)前條件變量基于的那個(gè)互斥鎖;
- 讓當(dāng)前的goroutine處于等待狀態(tài),等到通知到來時(shí)再?zèng)Q定是否喚醒它。此時(shí),這個(gè)goroutine就會(huì)阻塞在調(diào)用這個(gè)Wait方法的那行代碼上;
- 如果通知到來并決定喚醒這個(gè)goroutine,那么就在喚醒它之后重新鎖定當(dāng)前條件變量基于的互斥鎖。自此以后,當(dāng)前的goroutine就會(huì)繼續(xù)執(zhí)行后面的代碼了。
(2)為什么要先要鎖定條件變量基于的互斥鎖,才能調(diào)用它的wait方法
因?yàn)闂l件變量的wait方法在阻塞當(dāng)前的goroutine之前,會(huì)解鎖它基于的互斥鎖。所以在調(diào)用wait方法之前,必須先鎖定這個(gè)互斥鎖,否則在調(diào)用這個(gè)wait方法時(shí),就會(huì)引發(fā)一個(gè)不可恢復(fù)的panic。
如果條件變量的Wait方法不先解鎖互斥鎖的話,那就會(huì)造成兩個(gè)后果:不是當(dāng)前的程序因panic而崩潰,就是相關(guān)的goroutine全面阻塞。
(3)為什么用for語句來包裹調(diào)用的wait方法表達(dá)式,用if語句不行嗎
if語句只會(huì)對(duì)共享資源的狀態(tài)檢查一次,而for語句卻可以做多次檢查,直到這個(gè)狀態(tài)改變?yōu)橹埂?/p>
之所以做多次檢查,主要是為了保險(xiǎn)起見。如果一個(gè)goroutine因收到通知而被喚醒,但卻發(fā)現(xiàn)共享資源的狀態(tài),依然不符合它的要求i,那么就應(yīng)該再次調(diào)用條件變量的Wait方法,并繼續(xù)等待下次通知的到來。
這種情況是很有可能發(fā)生的,具體如下面所示:
- 有多個(gè) goroutine 在等待共享資源的同一種狀態(tài)。比如,它們都在等mailbox變量的值不為0的時(shí)候再把它的值變?yōu)?,這就相當(dāng)于有多個(gè)人在等著我向信箱里放置情報(bào)。雖然等待的 goroutine 有多個(gè),但每次成功的 goroutine 卻只可能有一個(gè)。別忘了,條件變量的Wait方法會(huì)在當(dāng)前的 goroutine 醒來后先重新鎖定那個(gè)互斥鎖。在成功的 goroutine 最終解鎖互斥鎖之后,其他的 goroutine 會(huì)先后進(jìn)入臨界區(qū),但它們會(huì)發(fā)現(xiàn)共享資源的狀態(tài)依然不是它們想要的。這個(gè)時(shí)候,for循環(huán)就很有必要了。
- 共享資源可能有的狀態(tài)不是兩個(gè),而是更多。比如,mailbox變量的可能值不只有0和1,還有2、3、4。這種情況下,由于狀態(tài)在每次改變后的結(jié)果只可能有一個(gè),所以,在設(shè)計(jì)合理的前提下,單一的結(jié)果一定不可能滿足所有 goroutine 的條件。那些未被滿足的 goroutine 顯然還需要繼續(xù)等待和檢查。
- 有一種可能,共享資源的狀態(tài)只有兩個(gè),并且每種狀態(tài)都只有一個(gè) goroutine 在關(guān)注,就像我們?cè)谥鲉栴}當(dāng)中實(shí)現(xiàn)的那個(gè)例子那樣。不過,即使是這樣,使用for語句仍然是有必要的。原因是,在一些多 CPU 核心的計(jì)算機(jī)系統(tǒng)中,即使沒有收到條件變量的通知,調(diào)用其Wait方法的 goroutine 也是有可能被喚醒的。這是由計(jì)算機(jī)硬件層面決定的,即使是操作系統(tǒng)(比如 Linux)本身提供的條件變量也會(huì)如此。
綜上所述,在包裹條件變量的Wait方法的時(shí)候,我們總是應(yīng)該使用for語句。
不要用if語句,因?yàn)樗荒苤貜?fù)地執(zhí)行“檢查狀態(tài) - 等待通知 - 被喚醒”的這個(gè)流程。
(4)條件變量的Signal方法和Broadcast方法
條件變量signal方法和Broadcast方法都是用來發(fā)送通知的,不同的是,前者的通知只會(huì)喚醒一個(gè)因此而等待的goroutine,而后者的通知卻會(huì)喚醒所有為此等待的goroutine。
條件變量的Wait方法總會(huì)把當(dāng)前的 goroutine 添加到通知隊(duì)列的隊(duì)尾,而它的Signal方法總會(huì)從通知隊(duì)列的隊(duì)首開始,查找可被喚醒的 goroutine。所以,因Signal方法的通知,而被喚醒的 goroutine 一般都是最早等待的那一個(gè)。
條件變量Signal方法和Broadcast方法放置的位置:
與Wait方法不同,條件變量的Signal方法和Broadcast方法并不需要在互斥鎖的保護(hù)下執(zhí)行。恰恰相反,我們最好在解鎖條件變量基于的那個(gè)互斥鎖之后,再去調(diào)用它的這兩個(gè)方法。這更有利于程序的運(yùn)行效率。
條件變量的通知具有即時(shí)性:
如果發(fā)送通知的時(shí)候沒有 goroutine 為此等待,那么該通知就會(huì)被直接丟棄。在這之后才開始等待的 goroutine 只可能被后面的通知喚醒。
到此這篇關(guān)于GO的條件變量sync.Cond的文章就介紹到這了,更多相關(guān)go 條件變量sync.Cond內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang?sync.Once實(shí)現(xiàn)單例模式的方法詳解
Go?語言的?sync?包提供了一系列同步原語,其中?sync.Once?就是其中之一。本文將深入探討?sync.Once?的實(shí)現(xiàn)原理和使用方法,幫助大家更好地理解和應(yīng)用?sync.Once,需要的可以參考一下2023-05-05使用Go實(shí)現(xiàn)TLS服務(wù)器和客戶端的示例
本文主要介紹了Go實(shí)現(xiàn)TLS服務(wù)器和客戶端的示例,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12golang原生http包實(shí)現(xiàn)各種情況的get請(qǐng)求方式
這篇文章主要介紹了golang原生http包實(shí)現(xiàn)各種情況的get請(qǐng)求方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08go local history本地歷史恢復(fù)代碼神器
這篇文章主要為大家介紹了go local history本地歷史恢復(fù)代碼神器的使用功能詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01VsCode搭建Go語言開發(fā)環(huán)境的配置教程
這篇文章主要介紹了在VsCode中搭建Go開發(fā)環(huán)境的配置教程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-05-05