[TOC]
# 簡介
~~~
var mailbox uint8
var lock sync.RWMutex
sendCond := sync.NewCond(&lock)
recvCond := sync.NewCond(lock.RLocker())
~~~
**本身不是鎖,要與鎖結合使用**
go標準庫中的sync.Cond類型代表了條件變量.
條件變量要與鎖(互斥鎖,或者讀寫鎖)一起使用.成員變量L代表與條件變量搭配使用的鎖
~~~
type Cond struct {
noCopy noCopy
L Locker
notify notifyList
checker copyChecker
}
~~~
對應有3個常用方法: Wait, Signal, Broadcast
~~~
func (c *Cond) Wait()
~~~
* 阻塞等待條件變量滿足,等醒
* 釋放已掌握的互斥鎖相當于cond.L.Unlock().**注意:1,2兩步為一個原子操作**
* 當被喚醒的時候,Wait()返回,解除阻塞并重新獲取互斥鎖.相當于cond.L.Lock()
為什么wait要做那3步操作,因為你在等待的時候,把鎖釋放掉啊,讓別人訪問公共空間,然后你被喚醒的時候,你需要拿到鎖,拿到鎖才能對公共空間訪問
~~~
func (c *Cond) Signal()
~~~
**Signal()通知的順序是根據原來加入通知列表(Wait())的先入先出**
**若沒有Wait(),也不會報錯**
**單發通知,一次一個**,給一個正在等待(阻塞)在該條件變量上的協程發送通知
~~~
func (c *Cond) Broadcast()
~~~
**廣播通知,都醒了,驚群**,給正在等待(阻塞)在該條件變量上的所有協程發送通知
# 生產者消費者

代碼注意點是,那里用for,不用for用if的haul,喚醒后往下執行,如果容量滿的話是會阻塞的,如果是for的話,wait好的話會再次判斷下的,if沒有再次判斷
用if的話,會出現問題而且是偶爾的出現,因為if里面如果喚醒,那么往下如果阻塞,阻塞的話,消費者無法喚醒他了,因為wait已經走過了
~~~
//創建全局條件變量
var cond sync.Cond
//生產者
func producer(out chan<- int, idx int) {
for {
//條件變量對應互斥鎖加鎖
cond.L.Lock()
//注意這邊用for不能用if
//循環判斷,如果條件不滿足直接跳過,滿足就等待,因為怕喚醒后有多個生產者一下子讓他充滿
//讓他解開的同時,順便判斷下,怕其他生產者已經寫到了3個
for len(out) == 3 { //產品區滿,等待消費者
cond.Wait() //掛起當前協程,等待條件變量滿足,被消費者喚醒
}
num := rand.Intn(1000) //產生一個隨機數
out <- num
fmt.Println("---生產者---產生數據---剩余多少個---", idx, num, len(out))
cond.L.Unlock() //生產結束,解鎖互斥鎖
cond.Signal() //喚醒阻塞的消費者
time.Sleep(time.Second)
}
}
//消費者
func consumer(in <-chan int, idx int) {
for {
//條件變量對應互斥鎖加鎖(與生產者是同一個)
cond.L.Lock()
//產品區為空,等待生產者生產
for len(in) == 0 {
cond.Wait()
}
//將channel中的數據讀取(消費)
num := <-in
fmt.Println("---消費者---消費數據---公共區剩余多少個---", idx, num, len(in))
//消費結束,解鎖互斥鎖
cond.L.Unlock()
//喚醒阻塞的生產者
cond.Signal()
//消費者休息一會兒,給其他協程機會
time.Sleep(time.Millisecond * 500)
}
}
func main() {
rand.Seed(time.Now().UnixNano())
//產品區(公共區)使用channel模擬
product := make(chan int, 3)
//創建互斥鎖和條件變量
cond.L = new(sync.Mutex)
//生產者
for i := 0; i < 5; i++ {
go producer(product, i+1)
}
//消費者
for i := 0; i < 3; i++ {
go consumer(product, i+1)
}
for {
;
}
}
~~~
# 注意點
我們在利用條件變量等待通知的時候,需要在它基于的那個互斥鎖保護下進行。而在進行單發通知或廣播通知的時候,卻是恰恰相反的,也就是說,需要在對應的互斥鎖解鎖之后再做這兩種操作。
---
條件變量并不是被用來保護臨界區和共享資源的,它是用于協調想要訪問共享資源的那些線程的。當共享資源的狀態發生變化時,它可以被用來通知被互斥鎖阻塞的線程。
---
把調用它的 goroutine(也就是當前的 goroutine)加入到當前條件變量的通知隊列中。
解鎖當前的條件變量基于的那個互斥鎖。
讓當前的 goroutine 處于等待狀態,等到通知到來時再決定是否喚醒它。此時,這個 goroutine 就會阻塞在調用這個Wait方法的那行代碼上。
---
如果通知到來并且決定喚醒這個 goroutine,那么就在喚醒它之后重新鎖定當前條件變量基于的互斥鎖。自此之后,當前的 goroutine 就會繼續執行后面的代碼了
---
如果一個 goroutine 因收到通知而被喚醒,但卻發現共享資源的狀態,依然不符合它的要求,那么就應該再次調用條件變量的Wait方法,并繼續等待下次通知的到來。
---
條件變量的Wait方法總會把當前的 goroutine 添加到通知隊列的隊尾,而它的Signal方法總會從通知隊列的隊首開始,查找可被喚醒的 goroutine。所以,因Signal方法的通知,而被喚醒的 goroutine 一般都是最早等待的那一個。
---
最后,請注意,**條件變量的通知具有即時性**。也就是說,如果發送通知的時候沒有 goroutine 為此等待,那么該通知就會被直接丟棄。在這之后才開始等待的 goroutine 只可能被后面的通知喚醒。
# 適合什么
條件變量適合保護那些可執行兩個對立操作的共享資源。比如,一個既可讀又可寫的共享文件。又比如,既有生產者又有消費者的產品池。
**盡量少的鎖爭**
相對應的,我們在調用條件變量的 Wait 方法的時候,應該處在其中的鎖的保護之下。因為有同一個鎖保護,所以不可能有多個 goroutine 同時執行到這個 Wait 方法調用,也就不可能存在針對其中鎖的重復解鎖。
對于同一個鎖,多個 goroutine 對它重復鎖定時只會有一個成功,其余的會阻塞;多個 goroutine 對它重復解鎖時也只會有一個成功,但其余的會拋 panic
- 基礎
- 簡介
- 主要特征
- 變量和常量
- 編碼轉換
- 數組
- byte與rune
- big
- sort接口
- 和mysql類型對應
- 函數
- 閉包
- 工作區
- 復合類型
- 指針
- 切片
- map
- 結構體
- sync.Map
- 隨機數
- 面向對象
- 匿名組合
- 方法
- 接口
- 權限
- 類型查詢
- 異常處理
- error
- panic
- recover
- 自定義錯誤
- 字符串處理
- 正則表達式
- json
- 文件操作
- os
- 文件讀寫
- 目錄
- bufio
- ioutil
- gob
- 棧幀的內存布局
- shell
- 時間處理
- time詳情
- time使用
- new和make的區別
- container
- list
- heap
- ring
- 測試
- 單元測試
- Mock依賴
- delve
- 命令
- TestMain
- path和filepath包
- log日志
- 反射
- 詳解
- plugin包
- 信號
- goto
- 協程
- 簡介
- 創建
- 協程退出
- runtime
- channel
- select
- 死鎖
- 互斥鎖
- 讀寫鎖
- 條件變量
- 嵌套
- 計算單個協程占用內存
- 執行規則
- 原子操作
- WaitGroup
- 定時器
- 對象池
- sync.once
- 網絡編程
- 分層模型
- socket
- tcp
- udp
- 服務端
- 客戶端
- 并發服務器
- Http
- 簡介
- http服務器
- http客戶端
- 爬蟲
- 平滑重啟
- context
- httptest
- 優雅中止
- web服務平滑重啟
- beego
- 安裝
- 路由器
- orm
- 單表增刪改查
- 多級表
- orm使用
- 高級查詢
- 關系查詢
- SQL查詢
- 元數據二次定義
- 控制器
- 參數解析
- 過濾器
- 數據輸出
- 表單數據驗證
- 錯誤處理
- 日志
- 模塊
- cache
- task
- 調試模塊
- config
- 部署
- 一些包
- gjson
- goredis
- collection
- sjson
- redigo
- aliyunoss
- 密碼
- 對稱加密
- 非對稱加密
- 單向散列函數
- 消息認證
- 數字簽名
- mysql優化
- 常見錯誤
- go run的錯誤
- 新手常見錯誤
- 中級錯誤
- 高級錯誤
- 常用工具
- 協程-泄露
- go env
- gometalinter代碼檢查
- go build
- go clean
- go test
- 包管理器
- go mod
- gopm
- go fmt
- pprof
- 提高編譯
- go get
- 代理
- 其他的知識
- go內存對齊
- 細節總結
- nginx路由匹配
- 一些博客
- redis為什么快
- cpu高速緩存
- 常用命令
- Go 永久阻塞的方法
- 常用技巧
- 密碼加密解密
- for 循環迭代變量
- 備注
- 垃圾回收
- 協程和纖程
- tar-gz
- 紅包算法
- 解決golang.org/x 下載失敗
- 逃逸分析
- docker
- 鏡像
- 容器
- 數據卷
- 網絡管理
- 網絡模式
- dockerfile
- docker-composer
- 微服務
- protoBuf
- GRPC
- tls
- consul
- micro
- crontab
- shell調用
- gorhill/cronexpr
- raft
- go操作etcd
- mongodb