14.4 使用 select 切換協程
從不同的併發執行的協程中獲取值可以通過關鍵字select
來完成,它和switch
控制語句非常相似(章節5.3)也被稱作通信開關;它的行爲像是“你準備好了嗎”的輪詢機制;select
監聽進入通道的數據,也可以是用通道發送值的時候。
select {
case u:= <- ch1:
...
case v:= <- ch2:
...
...
default: // no value ready to be received
...
}
default
語句是可選的;fallthrough 行爲,和普通的 switch 相似,是不允許的。在任何一個 case 中執行 break
或者 return
,select 就結束了。
select
做的就是:選擇處理列出的多個通信情況中的一個。
- 如果都阻塞了,會等待直到其中一個可以處理
- 如果多個可以處理,隨機選擇一個
- 如果沒有通道操作可以處理並且寫了
default
語句,它就會執行:default
永遠是可運行的(這就是準備好了,可以執行)。
在 select
中使用發送操作並且有 default
可以確保發送不被阻塞!如果沒有 case,select 就會一直阻塞。
select
語句實現了一種監聽模式,通常用在(無限)循環中;在某種情況下,通過 break
語句使循環退出。
在程序 goroutine_select.go 中有 2 個通道 ch1
和 ch2
,三個協程 pump1()
、pump2()
和 suck()
。這是一個典型的生產者消費者模式。在無限循環中,ch1
和 ch2
通過 pump1()
和 pump2()
填充整數;suck()
也是在無限循環中輪詢輸入的,通過 select
語句獲取 ch1
和 ch2
的整數並輸出。選擇哪一個 case 取決於哪一個通道收到了信息。程序在 main 執行 1 秒後結束。
示例 14.10-goroutine_select.go:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go pump1(ch1)
go pump2(ch2)
go suck(ch1, ch2)
time.Sleep(1e9)
}
func pump1(ch chan int) {
for i := 0; ; i++ {
ch <- i * 2
}
}
func pump2(ch chan int) {
for i := 0; ; i++ {
ch <- i + 5
}
}
func suck(ch1, ch2 chan int) {
for {
select {
case v := <-ch1:
fmt.Printf("Received on channel 1: %d\n", v)
case v := <-ch2:
fmt.Printf("Received on channel 2: %d\n", v)
}
}
}
輸出:
Received on channel 2: 5
Received on channel 2: 6
Received on channel 1: 0
Received on channel 2: 7
Received on channel 2: 8
Received on channel 2: 9
Received on channel 2: 10
Received on channel 1: 2
Received on channel 2: 11
...
Received on channel 2: 47404
Received on channel 1: 94346
Received on channel 1: 94348
一秒內的輸出非常驚人,如果我們給它計數(goroutine_select2.go),得到了 90000 個左右的數字。
練習:
練習 14.7:
- a)在練習 5.4 的 for_loop.go 中,有一個常見的 for 循環打印數字。在函數
tel
中實現一個 for 循環,用協程開始這個函數並在其中給通道發送數字。main()
線程從通道中獲取並打印。不要使用time.Sleep()
來同步:goroutine_panic.go - b)也許你的方案有效,可能會引發運行時的 panic:
throw:all goroutines are asleep-deadlock!
爲什麼會這樣?你如何解決這個問題?goroutine_close.go - c)解決 a)的另外一種方式:使用一個額外的通道傳遞給協程,然後在結束的時候隨便放點什麼進去。
main()
線程檢查是否有數據發送給了這個通道,如果有就停止:goroutine_select.go
練習 14.8:
從示例 6.10 的斐波那契程序開始,制定解決方案,使斐波那契週期計算獨立到協程中,並可以把結果發送給通道。
結束的時候關閉通道。main()
函數讀取通道並打印結果:goFibonacci.go
使用練習 6.9 中的算法寫一個更短的 gofibonacci2.go
使用 select
語句來寫,並讓通道退出(gofibonacci_select.go)
注意:當給結果計時並和 6.10 對比時,我們發現使用通道通信的性能開銷有輕微削減;這個例子中的算法使用協程並非性能最好的選擇;但是 gofibonacci3 方案使用了 2 個協程帶來了 3 倍的提速。
練習 14.9:
做一個隨機位生成器,程序可以提供無限的隨機 0 或者 1 的序列:random_bitgen.go
練習 14.10:polar_to_cartesian.go
(這是一種綜合練習,使用到第 4、9、11 章和本章的內容。)寫一個可交互的控制檯程序,要求用戶輸入二位平面極座標上的點(半徑和角度(度))。計算對應的笛卡爾座標系的點的 x 和 y 並輸出。使用極座標和笛卡爾座標的結構體。
使用通道和協程:
channel1
用來接收極座標channel2
用來接收笛卡爾座標
轉換過程需要在協程中進行,從 channel1 中讀取然後發送到 channel2。實際上做這種計算不提倡使用協程和通道,但是如果運算量很大很耗時,這種方案設計就非常合適了。
練習 14.11: concurrent_pi.go / concurrent_pi2.go
使用以下序列在協程中計算 pi:開啓一個協程來計算公式中的每一項並將結果放入通道,main()
函數收集並累加結果,打印出 pi 的近似值。
計算執行時間(參見第 6.11 節)
再次聲明這只是爲了一邊練習協程的概念一邊找點樂子。
如果你需要的話可使用 math.pi
中的 Pi;而且不使用協程會運算的更快。一個急速版本:使用 GOMAXPROCS
,開啓和 GOMAXPROCS
同樣多個協程。
習慣用法:後臺服務模式
服務通常是是用後臺協程中的無限循環實現的,在循環中使用 select
獲取並處理通道中的數據:
// Backend goroutine.
func backend() {
for {
select {
case cmd := <-ch1:
// Handle ...
case cmd := <-ch2:
...
case cmd := <-chStop:
// stop server
}
}
}
在程序的其他地方給通道 ch1
,ch2
發送數據,比如:通道 stop
用來清理結束服務程序。
另一種方式(但是不太靈活)就是(客戶端)在 chRequest
上提交請求,後臺協程循環這個通道,使用 switch
根據請求的行爲來分別處理:
func backend() {
for req := range chRequest {
switch req.Subjext() {
case A1: // Handle case ...
case A2: // Handle case ...
default:
// Handle illegal request ..
// ...
}
}
}
鏈接
- 目錄
- 上一節:通道的同步:關閉通道-測試阻塞的通道
- 下一節:通道,超時和計時器(Ticker)