通道是將goroutine的粘合劑,select語句是通道的粘合劑。后者讓我們能夠在項目中組合通道以形成更大的抽象來解決實際中遇到的問題。凸顯select語句在Go并發上的地位絕對不是言過其實。你可以在單個函數或類型定義中找到將本地通道綁定在一起的select語句,也可以在全局范圍找到連接系統級別兩個或多個組件的使用范例。除了連接組件外,在程序中的關鍵部分,select語句還可以幫助你安全地將通道與業務層面的概念(如取消,超時,等待和默認值)結合在一起。
既然select語句在Go中占有如此重要的地位——專門用于處理通道,那么你認為程序的組件應該如何協調? 我們將在第五章專門研究這個問題(提示:更喜歡使用頻道)。
那么這些強大的select語句是什么? 我們如何使用它們,它們是如何工作的? 我們先從一個簡單的示例開始:
```
var c1, c2 <-chan interface{}
var c3 chan<- interface{}
select {
case <-c1:
// Do something
case <-c2:
// Do something
case c3 <- struct{}{}:
// Do something
}
```
看著跟switch有點像,是吧。跟switch相同的是,select代碼塊也包含一系列case分支。跟switch不同的是,case分支不會被順序測試,如果沒有任何分支的條件可供滿足,select會一直等待直到某個case語句完成。
所有通道的讀取和寫入都被同時考慮,以查看它們中的任何一個是否準備好: 如果沒有任何通道準備就緒,則整個select語句將會阻塞。當一個通道準備好時,該操作將繼續,并執行相應的語句。 我們來看一個簡單的例子:
```
start := time.Now()
c := make(chan interface{})
go func() {
time.Sleep(5 * time.Second)
close(c) // 1
}()
fmt.Println("Blocking on read...")
select {
case <-c: // 2
fmt.Printf("Unblocked %v later.\n", time.Since(start))
}
```
1. 這里我們在5秒后關閉通道。
2. 在這里我們嘗試讀取通道。注意,盡管我們可以不使用select語句而直接使用<-c,但我們的目的是為了展示select語句。
這會輸出:
```
Blocking on read...
Unblocked 5s later.
```
如你所見,在5秒的阻塞后我們進入select代碼塊。這是一種簡單而有效的方式來實現阻塞等待,但如果反思一下,我們可以發現一些問題:
* 當多個通道需要讀取時會發生什么?
* 如果所有通道都尚未初始化完成,該怎么辦?
* 如果我們想做點什么,但當前通道還沒準備好呢?
第一個問題很有趣,讓我們試試看會發生什么:
```
c1 := make(chan interface{})
close(c1)
c2 := make(chan interface{})
close(c2)
var c1Count, c2Count int
for i := 1000; i >= 0; i-- {
select {
case <-c1:
c1Count++
case <-c2:
c2Count++
}
}
fmt.Printf("c1Count: %d\nc2Count: %d\n", c1Count, c2Count)
```
這會輸出:
```
c1Count: 505
c2Count: 496
```
c1和c2被關閉后,可以讀到其承載的類型的零值。你可以看到,在1001次循環中,大約有一半的時間從c1讀取,有一半是從c2讀取的。這似乎很有趣,也許有點太巧合。 事實上,是由Go的運行時導致的。Go運行時對一組case語句執行偽隨機統一選擇。這意味著在同樣的條件下,每個case被選中的機會幾乎是一樣的。
乍一看這似乎并不重要,但其背后的理由是令人難以置信的有意思。我們先陳述一個事實:Go運行時無法知道你的select語句的意圖;也就是說,它不能推斷出你的問題所在,或者你為什么將一組通道放在一個select語句中。正因為如此,Go運行時所能做的最好的事情就是在任何情況下運行良好。一個好的方法是在你的程序中中引入一個隨機變量——以決定選擇哪個case執行。通過加權平均使用每個通道的機會,使得所有使用select語句的Go程序表現良好。
第二個問題呢?如果所有通道都尚未初始化完成會發生什么?如果所有的通道都處在阻塞狀態,你無法進行處理,但你又不能就這樣持續阻塞下去,你可能希望程序能夠執行超時。Go的time包提供了一個很好的方式來完成這個功能,這些功能完全符合選擇語句的范式。 這里有一個例子:
```
var c <-chan int
select {
case <-c: //1
case <-time.After(1 * time.Second):
fmt.Println("Timed out.")
}
```
1. 這個case分支會永久阻塞,因為我們從一個nil通道讀取。
這會輸出:
```
Timed out.
```
time.After接收一個類型為time.After的參數,并返回一個通道,該通道將在你提供該通道的持續時間后發送當前時間。這提供了在選擇語句中超時的簡潔方法。我們將在第4章中重新討論這種模式,并討論針對此問題的更強大的解決方案。
我們還有最后的一個問題:如果我們想做點什么,但當前通道還沒準備好呢?select語句中允許我們添加default條件,以便你在所有分支都不符合執行條件的時候執行。這里有個例子:
```
start := time.Now()
var c1, c2 <-chan int
select {
case <-c1:
case <-c2:
default:
fmt.Printf("In default after %v\n\n", time.Since(start))
}
```
這會輸出:
```
In default after 1.421μs
```
你可以看到它幾乎是瞬間運行默認語句。這允許你在不阻塞的情況下退出選擇塊。 通常你會看到for-select循環結合使用。這使得goroutine可以在等待另一個goroutine報告結果的同時取得進展。 這是一個例子:
```
done := make(chan interface{})
go func() {
time.Sleep(5 * time.Second)
close(done)
}()
workCounter := 0
loop:
for {
select {
case <-done:
break loop
default:
}
// Simulate work
workCounter++
time.Sleep(1 * time.Second)
}
fmt.Printf("Achieved %v cycles of work before signalled to stop.\n", workCounter)
```
這會輸出:
```
Achieved 5 cycles of work before signalled to stop.
```
在這個例子中,我們有一個循環正在做某種工作,偶爾檢查它是否應該停止。
最后,空選擇語句有一個特殊情況:select語句沒有case子句。 這些看起來像這樣:
```
select {}
```
這條語句將永久阻塞。
在第6章中,我們將深入研究select語句的工作原理。 從更高層面來看,它可以幫助你安全高效地將各種概念和子系統組合在一起。
* * * * *
學識淺薄,錯誤在所難免。我是長風,歡迎來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運行時
- 任務調度