gopl-zh.github.com/ch8/ch8-07.md

135 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## 8.7. 基於select的多路複用
下面的程序會進行火箭發射的倒計時。time.Tick函數返迴一個channel程序會週期性地像一個節拍器一樣向這個channel發送事件。每一個事件的值是一個時間戳不過更有意思的是其傳送方式。
```go
gopl.io/ch8/countdown1
func main() {
fmt.Println("Commencing countdown.")
tick := time.Tick(1 * time.Second)
for countdown := 10; countdown > 0; countdown-- {
fmt.Println(countdown)
j<-tick
}
launch()
}
```
現在我們讓這個程序支持在倒計時中用戶按下return鍵時直接中斷發射流程。首先我們啟動一個goroutine這個goroutine會嚐試從標準輸入中調入一個單獨的byte併且如果成功了會向名爲abort的channel發送一個值。
```go
gopl.io/ch8/countdown2
abort := make(chan struct{})
go func() {
os.Stdin.Read(make([]byte, 1)) // read a single byte
abort <- struct{}{}
}()
```
現在每一次計數循環的迭代都需要等待兩個channel中的其中一個返迴事件了ticker channel當一切正常時(就像NASA jorgon的"nominal",譯註:這梗估計我們是不懂了)或者異常時返迴的abort事件。我們無法做到從每一個channel中接收信息如果我們這麽做的話如果第一個channel中沒有事件發過來那麽程序就會立刻被阻塞這樣我們就無法收到第二個channel中發過來的事件。這時候我們需要多路複用(multiplex)這些操作了爲了能夠多路複用我們使用了select語句。
```go
select {
case <-ch1:
// ...
case x := <-ch2:
// ...use x...
case ch3 <- y:
// ...
default:
// ...
}
```
上面是select語句的一般形式。和switch語句稍微有點相似也會有幾個case和最後的default選擇支。每一個case代表一個通信操作(在某個channel上進行發送或者接收)併且會包含一些語句組成的一個語句塊。一個接收表達式可能隻包含接收表達式自身(譯註:不把接收到的值賦值給變量什麽的)就像上面的第一個case或者包含在一個簡短的變量聲明中像第二個case里一樣第二種形式讓你能夠引用接收到的值。
select會等待case中有能夠執行的case時去執行。當條件滿足時select才會去通信併執行case之後的語句這時候其它通信是不會執行的。一個沒有任何case的select語句寫作select{},會永遠地等待下去。
讓我們迴到我們的火箭發射程序。time.After函數會立卽返迴一個channel併起一個新的goroutine在經過特定的時間後向該channel發送一個獨立的值。下面的select語句會會一直等待到兩個事件中的一個到達無論是abort事件或者一個10秒經過的事件。如果10秒經過了還沒有abort事件進入那麽火箭就會發射。
```go
func main() {
// ...create abort channel...
fmt.Println("Commencing countdown. Press return to abort.")
select {
case <-time.After(10 * time.Second):
// Do nothing.
case <-abort:
fmt.Println("Launch aborted!")
return
}
launch()
}
```
下面這個例子更微秒。ch這個channel的buffer大小是1所以會交替的爲空或爲滿所以隻有一個case可以進行下去無論i是奇數或者偶數它都會打印0 2 4 6 8。
```go
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch:
fmt.Println(x) // "0" "2" "4" "6" "8"
case ch <- i:
}
}
```
如果多個case同時就緒時select會隨機地選擇一個執行這樣來保證每一個channel都有平等的被select的機會。增加前一個例子的buffer大小會使其輸出變得不確定因爲當buffer旣不爲滿也不爲空時select語句的執行情況就像是拋硬幣的行爲一樣是隨機的。
下面讓我們的發射程序打印倒計時。這里的select語句會使每次循環迭代等待一秒來執行退出操作。
```go
gopl.io/ch8/countdown3
func main() {
// ...create abort channel...
fmt.Println("Commencing countdown. Press return to abort.")
tick := time.Tick(1 * time.Second)
for countdown := 10; countdown > 0; countdown-- {
fmt.Println(countdown)
select {
case <-tick:
// Do nothing.
case <-abort:
fmt.Println("Launch aborted!")
return
}
}
launch()
}
```
time.Tick函數表現得好像它創建了一個在循環中調用time.Sleep的goroutine每次被喚醒時發送一個事件。當countdown函數返迴時它會停止從tick中接收事件但是ticker這個goroutine還依然存活繼續徒勞地嚐試從channel中發送值然而這時候已經沒有其它的goroutine會從該channel中接收值了--這被稱爲goroutine洩露(§8.4.4)。
Tick函數挺方便但是隻有當程序整個生命週期都需要這個時間時我們使用它才比較合適。否則的話我們應該使用下面的這種模式
```go
ticker := time.NewTicker(1 * time.Second)
<-ticker.C // receive from the ticker's channel
ticker.Stop() // cause the ticker's goroutine to terminate
```
有時候我們希望能夠從channel中發送或者接收值併避免因爲發送或者接收導致的阻塞尤其是當channel沒有準備好寫或者讀時。select語句就可以實現這樣的功能。select會有一個default來設置當其它的操作都不能夠馬上被處理時程序需要執行哪些邏輯。
下面的select語句會在abort channel中有值時從其中接收值無值時什麽都不做。這是一個非阻塞的接收操作反複地做這樣的操作叫做“輪詢channel”。
```go
select {
case <-abort:
fmt.Printf("Launch aborted!\n")
return
default:
// do nothing
}
```
channel的零值是nil。也許會讓你覺得比較奇怪nil的channel有時候也是有一些用處的。因爲對一個nil的channel發送和接收操作會永遠阻塞在select語句中操作nil的channel永遠都不會被select到。
這使得我們可以用nil來激活或者禁用case來達成處理其它輸入或輸出事件時超時和取消的邏輯。我們會在下一節中看到一個例子。
練習8.8: 使用select來改造8.3節中的echo服務器爲其增加超時這樣服務器可以在客戶端10秒中沒有任何喊話時自動斷開連接。