這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
在編寫 golang 程式的過程中,channel 會經常使用。本文對 channel 的使用的確很特別,同時也非常實用。
原文在此:http://dave.cheney.net/2013/04/30/curious-channels
————翻譯分隔線————
絕妙的 channel
在 Go 程式設計語言中,channel 是一個閃耀的特性。它提供了一種強大的、在不使用鎖或臨界區的情況下,從某個 goroutine 向其他 goroutine 發送資料流的方法。
今天我想討論關於 channel 的兩個重要的特性,這些特性不但使其在控制資料流方面極為有用,而且用在流程式控制制方面也十分有效。
一個已經被關閉的 channel 永遠都不會阻塞
第一個特性,我想談一談已經被關閉的 channel。當一個 channel 一旦被關閉,就不能再向這個 channel 發送資料,不過你仍然可以嘗試從 channel 中擷取值。
package mainimport "fmt"func main() { ch := make(chan bool, 2) ch <- true ch <- true close(ch) for i := 0; i < cap(ch) +1 ; i++ { v, ok := <- ch fmt.Println(v, ok) }}
在這個例子裡,我們建立了一個緩衝區為兩個值的 channel,填充緩衝區並且關閉掉它。
true truetrue truefalse false
執行這個程式,首先會向我們展示那兩個發送到 channel 的值,然後第三次在 channel 上的嘗試會返回 flase 和 false。第一個 false 是 channel 類型的零值,channel 的類型是 chan bool,那麼就是 false。第二個表示 channel 的啟用狀態,當前是 false,表示 channel 被關閉。channel 會一直返回這些值。作為嘗試,可以修改這個例子使其從 channel 裡取 100 次值看看。
能夠檢測 channel 是否關閉是一個很有用的特性,可用於對 channel 進行 range 操作,並且當 channel 清空後退出迴圈。
package mainimport "fmt"func main() { ch := make(chan bool, 2) ch <- true ch <- true close(ch) for v := range ch { fmt.Println(v) // 被調用兩次 }}
但是其真正的價值是與 select 聯合時體現的。先從這個例子開始
package mainimport ( "fmt" "sync" "time")func main() { finish := make(chan bool) var done sync.WaitGroup done.Add(1) go func() { select { case <-time.After(1 * time.Hour): case <-finish: } done.Done() }() t0 := time.Now() finish <- true // 發送關閉訊號 done.Wait() // 等待 goroutine 結束 fmt.Printf("Waited %v for goroutine to stop\n", time.Since(t0))}
在我的系統上,這個程式的運行用了很短的等待延遲,因此很明顯 goroutine 不會等待整整一個小時,然後調用 done.Done()
Waited 129.607us for goroutine to stop
但是這個程式裡存在一些問題。首先是 finish channel 是不帶緩衝的,因此如果接收方忘記在其 select 語句中添加 finish,向其發送資料可能會導致阻塞。可以通過對要發送到的 select 塊進行封裝,以確保不會阻塞,或者設定 finish channel 帶有緩衝來解決這個問題。然而,如果有許多 goroutine 都監聽在 finish channel 上,那就需要跟蹤這個情況,並記得發送正確數量的資料到 finish channel。如果無法控制 goroutine 的建立會很棘手;同時它們也可能是由程式的另一部分來建立的,例如在響應網路請求的時候。
對於這個問題,一個很好的解決方案是利用已經被關閉的 channel 會即時返回這一機制。使用這個特性改寫程式,現在包含了 100 個 goroutine,而無需跟蹤 goroutine 產生的數量,或調整 finish channel 的大小。
package mainimport ( "fmt" "sync" "time")func main() { const n = 100 finish := make(chan bool) var done sync.WaitGroup for i := 0; i < n; i++ { done.Add(1) go func() { select { case <-time.After(1 * time.Hour): case <-finish: } done.Done() }() } t0 := time.Now() close(finish) // 關閉 finish 使其立即返回 done.Wait() // 等待所有的 goroutine 結束 fmt.Printf("Waited %v for %d goroutines to stop\n", time.Since(t0), n)}
在我的系統上,它返回
Waited 231.385us for 100 goroutines to stop
那麼這裡發生了什嗎?當 finish channel 被關閉後,它會立刻返回。那麼所有等待接收 time.After channel 或 finish 的 goroutine 的 select 語句就立刻完成了,並且 goroutine 在調用 done.Done() 來減少 WaitGroup 計數器後退出。這個強大的機制在無需知道未知數量的 goroutine 的任何細節而向它們發送訊號而成為可能,同時也不用擔心死結。
在進入下一個話題前,再來看一個許多 Go 程式員都喜愛的簡單樣本。在上面的例子中,從未向 finish channel 發送資料,接受方也將收到的任何資料全部丟棄。因此將程式寫成這樣就很正常了:
package mainimport ( "fmt" "sync" "time")func main() { finish := make(chan struct{}) var done sync.WaitGroup done.Add(1) go func() { select { case <-time.After(1 * time.Hour): case <-finish: } done.Done() }() t0 := time.Now() close(finish) done.Wait() fmt.Printf("Waited %v for goroutine to stop\n", time.Since(t0))}
當 close(finish) 依賴於關閉 channel 的訊息機制,而沒有資料收發時,將 finish 定義為 type chan struct{} 表示 channel 沒有任何資料;只對其關閉的特性感興趣。
一個 nil channel 永遠都是阻塞的
我想談的第二個特性正好與已經關閉的 channel 的特性正好相反。一個 nil channel;當 channel 的值尚未進行初始化或賦值為 nil 是,永遠都是阻塞的。例如
package mainfunc main() { var ch chan bool ch <- true // 永遠阻塞}
當 ch 為 nil 時將會死結,並且永遠都不會發送資料。對於接收是一樣的
package mainfunc main() { var ch chan bool <- ch // 永遠阻塞}
這看起來似乎並不怎麼重要,但是當使用已經關閉的 channel 機制來等待多個 channel 關閉的時候,這確實是一個很有用的特性。例如
// WaitMany 等待 a 和 b 關閉。func WaitMany(a, b chan bool) { var aclosed, bclosed bool for !aclosed || !bclosed { select { case <-a: aclosed = true case <-b: bclosed = true } }}
WaitMany() 用於等待 channel a 和 b 關閉是個不錯的方法,但是有一個問題。假設 channel a 首先被關閉,然後它會立刻返回。但是由於 bclosed 仍然是 false,程式會進入死迴圈,而讓 channel b 永遠不會被判定為關閉。
一個解決這個問題的安全的方法是利用 nil channel 的阻塞特性,並且將程式重寫如下
package mainimport ( "fmt" "time")func WaitMany(a, b chan bool) { for a != nil || b != nil { select { case <-a: a = nil case <-b: b = nil } }}func main() { a, b := make(chan bool), make(chan bool) t0 := time.Now() go func() { close(a) close(b) }() WaitMany(a, b) fmt.Printf("waited %v for WaitMany\n", time.Since(t0))}
在重寫的 WaitMany() 中,一旦接收到一個值,就將 a 或 b 的引用設定為 nil。當 nil channel 是 select 語句的一部分時,它實際上會被忽略,因此,將 a 設定為 nil 便會將其從 select 中移除,僅僅留下 b 等待它被關閉,進而退出迴圈。
在我的系統上運行得到
waited 54.912us for WaitMany
總結來說,正是關閉和 nil channlechannel 這些特性非常簡單,使得它們成為建立高並發的程式的強有力的構件。