Cond的文檔很好的描述了其存在的目的:
> Cond實現了一個條件變量,用于等待或宣布事件發生時goroutine的交匯點。
在這個定義中,“事件”是指兩個或更多的goroutine之間的任何信號,僅指事件發生了,不包含其他任何信息。 通常,你可能想要在收到某個goroutine信號前令其處于等待狀態。 如果我們要在不使用Cond的情況下實現這一點,那么一個粗暴的方法就是使用無限循環:
```
for conditionTrue() == false {
}
```
然而這會導致消耗一個內核的所有周期。我們可以引入time.sleep來改善這一點:
```
for conditionTrue() == false {
time.Sleep(1 * time.Millisecond)
}
```
這樣就看起來好點了,但執行效率依然很低效,而且你需要顯示標明需要休眠多久:太長或太短都會不必要的消耗無謂的CPU時間。如果有一種方法可以讓goroutine有效地睡眠,直到喚醒并檢查其狀態,那將會更好。這種需求簡直是為Cond量身定制的,使用它我們可以這樣改造上面的例子:
```
c := sync.NewCond(&sync.Mutex{}) // 1
c.L.Lock() // 2
for conditionTrue() == false {
c.Wait() // 3
}
c.L.Unlock() // 4
```
1. 這里我們實例化一個新的Cond。NewCond函數傳入的參數實現了sync.Locker類型。Cond類型允許以并行安全的方式與其他goroutines協調。
2. 在這里我們進行鎖定。這一步很必要,因為Wait的調用會執行解鎖并暫停該goroutine。
3. 在這里我們進入暫停狀態,這是阻塞的,直到接收到通知。
4. 這里執行解鎖,這一步很必要,因為當調用退出時,它會c.L上調用Lock。
這個例子相對之前的效率就比較高了。請注意,對Wait的調用不僅僅是阻塞,它暫停當前的goroutine,允許其他goroutine在操作系統線程上運行。當你調用Wait時,還會發生其他一些事情:進入Wait后,Cond的變量Locker將調用Unlock,并在退出Wait時,Cond變量的Locker上會調用Lock。 在我看來,這有點讓人不習慣; 這實際上是該方法的隱藏副作用。 看起來我們在等待條件發生的整個過程中都持有這個鎖,但事實并非如此。 當你檢查代碼時,需要留意這一點。
讓我們擴展這個例子,來看看等待信號的goroutine和發送信號的goroutine該怎么寫。假設我們有一個固定長度為2的隊列,并且我們要將10個元素放入隊列中。 我們希望一有空間就能放入,所以在隊列中有空間時需要立刻通知:
```
c := sync.NewCond(&sync.Mutex{}) //1
queue := make([]interface{}, 0, 10) //2
removeFromQueue := func(delay time.Duration) {
time.Sleep(delay)
c.L.Lock() //8
queue = queue[1:] //9
fmt.Println("Removed from queue")
c.L.Unlock() //10
c.Signal() //11
}
for i := 0; i < 10; i++ {
c.L.Lock() //3
for len(queue) == 2 { //4
c.Wait() //5
}
fmt.Println("Adding to queue")
queue = append(queue, struct{}{})
go removeFromQueue(1 * time.Second) //6
c.L.Unlock() //7
}
```
1. 首先,我們使用一個標準的sync.Mutex作為Locker來創建Cond。
2. 接下來,我們創建一個長度為零的切片。 由于我們知道最終會添加10個元素,因此我們將其容量設為10。
3. 在進入關鍵的部分前調用Lock來鎖定c.L。
4. 在這里我們檢查隊列的長度,以確認什么時候需要等待。由于removeFromQueue是異步的,*for不滿足時才會跳出,而if做不到重復判斷*,這一點很重要。
5. 調用Wait,這將阻塞main goroutine,直到接受到信號。
6. 這里我們創建一個新的goroutine,它會在1秒后將元素移出隊列。
7. 這里我們退出條件的關鍵部分,因為我們已經成功加入了一個元素。
8. 我們再次進入該并發條件下的關鍵部分,以修改與并發條件判斷直接相關的數據。
9. 在這里,我們移除切片的頭部并重新分配給第二個元素,這一步模擬了元素出列。
10. 我們退出操作關鍵部分,因為我們已經成功移除了一個元素。
11. 這里,我們發出信號,通知處于等待狀態的goroutine可以進行下一步了。
這會輸出:
```
Adding to queue Adding to queue Removed from queue Adding to queue Removed from queue Adding to queue Removed from queue Adding to queue Removed from queue Adding to queue Removed from queue Adding to queue Removed from queue Adding to queue Removed from queue Adding to queue Removed from queue Adding to queue
```
***強烈建議將代碼中for換成if感受下,作者考慮的真周全***
正如你所看到的,程序成功地將所有10個元素添加到隊列中(并且在它有機會在最后兩項出隊之前退出)。 它也會持續等待,直到至少有一個元素在放入另一個元素之前出列。
在這個例子中,我們使用了一個新的方法,Signal。這是Cond類型提供的兩種通知方法之一,用于通知在等待調用上阻塞的goroutines條件已被觸發。另一種方法是Broadcast。在內部,運行時維護一個等待信號發送的goroutines的FIFO列表; Signal尋找等待時間最長的goroutine并通知,而Broadcast向所有處在等待狀態的goroutine發送信號。Broadcast可以說是兩種方法中最有趣的方式,因為它提供了一種同時與多個goroutine進行通信的解決方案。 我們可以通過通道輕松地再現Signal(隨后我們會看到這個例子),但是再現對Broadcast重復呼叫的行為將很困難。另外,Cond類型比使用通道更高效。
為了了解Broadcast是如何使用的,假設我們正在創建一個帶有按鈕的GUI程序,該程序需要注冊任意數量的函數,當點擊按鈕時運行這些函數。可以使用Cond的Brocast來通知所有已注冊函數,讓我們看看該如何實現:
```
type Button struct {
//1
Clicked *sync.Cond
}
button := Button{Clicked: sync.NewCond(&sync.Mutex{})}
subscribe := func(c *sync.Cond, fn func()) { //2
var tempwg sync.WaitGroup
tempwg.Add(1)
go func() {
tempwg.Done()
c.L.Lock()
defer c.L.Unlock()
c.Wait()
fn()
}()
tempwg.Wait()
}
var wg sync.WaitGroup //3
wg.Add(3)
subscribe(button.Clicked, func() { //4
fmt.Println("Maximizing window.")
wg.Done()
})
subscribe(button.Clicked, func() { //5
fmt.Println("Displaying annoying dialog box!")
wg.Done()
})
subscribe(button.Clicked, func() { //6
fmt.Println("Mouse clicked.")
wg.Done()
})
button.Clicked.Broadcast() //7
wg.Wait()
```
1. 我們定義一個Button類型,包含了sync.Cond指針類型的Clicked屬性,這是goroutine接收通知的關鍵條件。
2. 這里我們定義了一個較為簡單的函數,它允許我們注冊函數來處理信號。每個注冊的函數都在自己的goroutine上運行,并且在該goroutine不會退出,直到接收到通知。
3. 在這里,我們為按鈕點擊設置了一個處理程序。 它反過來在Clicked Cond上調用Broad cast以讓所有注冊函數知道按鈕已被點擊。
4. 這里我們創建一個WaitGroup。 這只是為了確保我們的程序在寫入標準輸出之前不會退出。
5. 在這里我們注冊一個處理函數,模擬點擊時最大化窗口。
6. 在這里我們注冊一個處理函數,模擬點擊時顯示對話框。
7. 接下來,我們模擬按鈕被點擊。
這會輸出:
```
Mouse clicked.
Maximizing window.
Displaying annoying dialog box!
```
可以看到,通過調用Broadcast,三個處理函數都運行了。如果不是wg WaitGroup,我們可以多次調button.Clicked.Broadcast(),并且每次都將運行這三個處理函數。 這是通道難以做到的,也是使用Cond類型的優勢之一。
* * * * *
學識淺薄,錯誤在所難免。我是長風,歡迎來Golang中國的群(211938256)就本書提出修改意見。
- 前序
- 誰適合讀這本書
- 章節導讀
- 在線資源
- 第一章 并發編程介紹
- 摩爾定律,可伸縮網絡和我們所處的困境
- 為什么并發編程如此困難
- 數據競爭
- 原子性
- 內存訪問同步
- 死鎖,活鎖和鎖的饑餓問題
- 死鎖
- 活鎖
- 饑餓
- 并發安全性
- 優雅的面對復雜性
- 第二章 代碼建模:序列化交互處理
- 并發與并行
- 什么是CSP
- CSP在Go中的衍生物
- Go的并發哲學
- 第三章 Go的并發構建模塊
- Goroutines
- sync包
- WaitGroup
- Mutex和RWMutex
- Cond
- Once
- Pool
- Channels
- select語句
- GOMAXPROCS
- 結論
- 第四章 Go的并發編程范式
- 訪問范圍約束
- fo-select循環
- 防止Goroutine泄漏
- or-channel
- 錯誤處理
- 管道
- 構建管道的最佳實踐
- 便利的生成器
- 扇入扇出
- or-done-channel
- tee-channel
- bridge-channel
- 隊列
- context包
- 小結
- 第五章 可伸縮并發設計
- 錯誤傳遞
- 超時和取消
- 心跳
- 請求并發復制處理
- 速率限制
- Goroutines異常行為修復
- 本章小結
- 第六章 Goroutines和Go運行時
- 任務調度