網站首頁 編程語言 正文
GO的條件變量
一、條件變量與互斥鎖
- 條件變量是基于互斥鎖的,它必須基于互斥鎖才能發揮作用;
- 條件變量并不是用來保護臨界區和共享資源的,它是用來協調想要訪問共享資源的那些線程的;
- 在Go語言中,條件變量最大的優勢是效率方面的提升。當共享資源不滿足條件的時候,想操作它的線程不用循環往返地檢查了,只要等待通知就好了。
二、條件變量與互斥鎖的配合使用
條件變量的初始化離不開互斥鎖,并且它的方法有點也是基于互斥鎖的。
條件變量提供的三個方法:等待通知(wait)、單發通知(signal)、廣發通知(broadcast)。
三、條件變量的使用
(1)創建鎖和條件
// mailbox 代表信箱 // 0 代表信箱是空的,1代表信箱是滿的 var mailbox uint8 // lock 代表信箱上的鎖 var lock sync.RWMutex // sendCond 代表專用于發信的條件變量 var sendCond = sync.NewCond(&lock) // reveCond 代表專用于收信的條件變量 var reveCond = sync.NewCond(lock.RLocker())
-
sync.Cond
類型并不是開箱即用的,只能利用sync.NewCond
創建它的指針值。這個函數需要sync.Locker
類型的參數值。 -
sync.Locker
是一個接口,它包含兩個指針方法,即Lock()
和Unlock()
;因此,sync.Mutex
和sync.RWMutex
這兩個類型的指針類型才是sync.Locker
接口的實現類型。 - 上面lock變量的Lock方法和Unlock方法分別用于對其中寫鎖的鎖定和解鎖,它們與sendCond變量的含義對應。
-
lock.RLocker()
得到的值,擁有Lock和Unlock方法,其內部會分別調用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 代表專用于發信的條件變量 var sendCond = sync.NewCond(&lock) // reveCond 代表專用于收信的條件變量 var reveCond = sync.NewCond(lock.RLocker()) // sign 用于傳遞演示完成的信號 sign := make(chan struct{}, 2) max := 5 go func(max int) { // 用于發信 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方法主要做了四件事:
- 把調用它的goroutine(也就是當前goroutine)加入到當前條件變量的通知隊列中;
- 解鎖當前條件變量基于的那個互斥鎖;
- 讓當前的goroutine處于等待狀態,等到通知到來時再決定是否喚醒它。此時,這個goroutine就會阻塞在調用這個Wait方法的那行代碼上;
- 如果通知到來并決定喚醒這個goroutine,那么就在喚醒它之后重新鎖定當前條件變量基于的互斥鎖。自此以后,當前的goroutine就會繼續執行后面的代碼了。
(2)為什么要先要鎖定條件變量基于的互斥鎖,才能調用它的wait方法
因為條件變量的wait方法在阻塞當前的goroutine之前,會解鎖它基于的互斥鎖。所以在調用wait方法之前,必須先鎖定這個互斥鎖,否則在調用這個wait方法時,就會引發一個不可恢復的panic。
如果條件變量的Wait方法不先解鎖互斥鎖的話,那就會造成兩個后果:不是當前的程序因panic而崩潰,就是相關的goroutine全面阻塞。
(3)為什么用for語句來包裹調用的wait方法表達式,用if語句不行嗎
if語句只會對共享資源的狀態檢查一次,而for語句卻可以做多次檢查,直到這個狀態改變為止。
之所以做多次檢查,主要是為了保險起見。如果一個goroutine因收到通知而被喚醒,但卻發現共享資源的狀態,依然不符合它的要求i,那么就應該再次調用條件變量的Wait方法,并繼續等待下次通知的到來。
這種情況是很有可能發生的,具體如下面所示:
- 有多個 goroutine 在等待共享資源的同一種狀態。比如,它們都在等mailbox變量的值不為0的時候再把它的值變為0,這就相當于有多個人在等著我向信箱里放置情報。雖然等待的 goroutine 有多個,但每次成功的 goroutine 卻只可能有一個。別忘了,條件變量的Wait方法會在當前的 goroutine 醒來后先重新鎖定那個互斥鎖。在成功的 goroutine 最終解鎖互斥鎖之后,其他的 goroutine 會先后進入臨界區,但它們會發現共享資源的狀態依然不是它們想要的。這個時候,for循環就很有必要了。
- 共享資源可能有的狀態不是兩個,而是更多。比如,mailbox變量的可能值不只有0和1,還有2、3、4。這種情況下,由于狀態在每次改變后的結果只可能有一個,所以,在設計合理的前提下,單一的結果一定不可能滿足所有 goroutine 的條件。那些未被滿足的 goroutine 顯然還需要繼續等待和檢查。
- 有一種可能,共享資源的狀態只有兩個,并且每種狀態都只有一個 goroutine 在關注,就像我們在主問題當中實現的那個例子那樣。不過,即使是這樣,使用for語句仍然是有必要的。原因是,在一些多 CPU 核心的計算機系統中,即使沒有收到條件變量的通知,調用其Wait方法的 goroutine 也是有可能被喚醒的。這是由計算機硬件層面決定的,即使是操作系統(比如 Linux)本身提供的條件變量也會如此。
綜上所述,在包裹條件變量的Wait方法的時候,我們總是應該使用for語句。
不要用if語句,因為它不能重復地執行“檢查狀態 - 等待通知 - 被喚醒”的這個流程。
(4)條件變量的Signal方法和Broadcast方法
條件變量signal方法和Broadcast方法都是用來發送通知的,不同的是,前者的通知只會喚醒一個因此而等待的goroutine,而后者的通知卻會喚醒所有為此等待的goroutine。
條件變量的Wait方法總會把當前的 goroutine 添加到通知隊列的隊尾,而它的Signal方法總會從通知隊列的隊首開始,查找可被喚醒的 goroutine。所以,因Signal方法的通知,而被喚醒的 goroutine 一般都是最早等待的那一個。
條件變量Signal方法和Broadcast方法放置的位置:
與Wait方法不同,條件變量的Signal方法和Broadcast方法并不需要在互斥鎖的保護下執行。恰恰相反,我們最好在解鎖條件變量基于的那個互斥鎖之后,再去調用它的這兩個方法。這更有利于程序的運行效率。
條件變量的通知具有即時性:
如果發送通知的時候沒有 goroutine 為此等待,那么該通知就會被直接丟棄。在這之后才開始等待的 goroutine 只可能被后面的通知喚醒。
原文鏈接:https://blog.csdn.net/hefrankeleyn/article/details/128602812
相關推薦
- 2022-11-23 C語言學習之關鍵字的示例詳解_C 語言
- 2022-05-12 C++深淺拷貝和string類的兩種寫法詳解_C 語言
- 2022-04-20 Python設計模式中的結構型適配器模式_python
- 2022-09-30 python計算列表元素與乘積詳情_python
- 2022-07-04 linux?shell?解析命令行參數及while?getopts用法小結_linux shell
- 2022-07-23 SQL?Server中T-SQL標識符介紹與無排序生成序號的方法_MsSql
- 2021-12-11 關于docker容器部署redis步驟介紹_docker
- 2022-10-09 ASP.NET泛型四之使用Lazy<T>實現延遲加載_實用技巧
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支