這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
0. 引言
channel 是 Go 語言中的一個非常重要的特性,這篇文章來深入瞭解一下 channel。
1. CSP
要想理解 channel 要Crowdsourced Security Testing道 CSP 模型。CSP 是 Communicating Sequential Process 的簡稱,中文可以叫做通訊順序進程,是一種並發編程模型,由 Tony Hoare 於 1977 年提出。簡單來說,CSP 模型由並發執行的實體(線程或者進程)所組成,實體之間通過發送訊息進行通訊,這裡發送訊息時使用的就是通道,或者叫 channel。CSP 模型的關鍵是關注 channel,而不關注發送訊息的實體。Go 語言實現了 CSP 部分理論,goroutine 對應 CSP 中並發執行的實體,channel 也就對應著 CSP 中的 channel。
2. channel 基礎知識
2.1 建立 channel
channel 使用之前需要通過 make 建立。
1 2
|
unBufferChan := make(chan int) // 1 bufferChan := make(chan int, N) // 2
|
上面的方式 1 建立的是無緩衝 channel,方式 2 建立的是緩衝 channel。如果使用 channel 之前沒有 make,會出現 dead lock 錯誤。至於為什麼是 dead lock,下文我們從源碼裡面看看。
1 2 3 4 5 6 7
|
func main() { var x chan int go func() { x <- 1 }() <-x }
|
1 2 3 4 5 6 7 8 9
|
$ go run channel1.go fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive (nil chan)]: main.main() /Users/kltao/code/go/examples/channl/channel1.go:11 +0x60
goroutine 4 [chan send (nil chan)]: main.main.func1(0x0)
|
2.2 channel 讀寫操作
1 2 3 4 5 6 7
|
ch := make(chan int, 10)
// 讀操作 x <- ch
// 寫操作 ch <- x
|
2.3 channel 種類
channel 分為無緩衝 channel 和有緩衝 channel。兩者的區別如下:
2.4 關閉 channel
channel 可以通過 built-in 函數 close() 來關閉。
1 2 3 4
|
ch := make(chan int)
// 關閉 close(ch)
|
關於關閉 channel 有幾點需要注意的是:
- 重複關閉 channel 會導致 panic。
- 向關閉的 channel 發送資料會 panic。
- 從關閉的 channel 讀資料不會 panic,但是讀出的資料是 channel 類似的預設值,比如 chan int 類型的 channel 關閉之後讀取到的值為 0。
對於上面的第三點,我們需要區分一下:channel 中的值是預設值還是 channel 關閉了。可以使用 ok-idiom 方式,這種方式在 map 中比較常用。
1 2 3 4 5 6 7 8 9
|
ch := make(chan int, 10) ... close(ch)
// ok-idiom val, ok := <-ch if ok == false { // channel closed }
|
3. channel 的典型用法
1. goroutine 通訊
1 2 3 4 5 6 7
|
func main() { x := make(chan int) go func() { x <- 1 }() <-x }
|
2. select
select 一定程度上可以類比於 linux 中的 IO 多工中的 select。後者相當於提供了對多個 IO 事件的統一管理,而 Golang 中的 select 相當於提供了對多個 channel 的統一管理。當然這隻是 select 在 channel 上的一種使用方法。
1 2 3 4 5 6 7
|
select { case e, ok := <-ch1: ... case e, ok := <-ch2: ... default: }
|
值得注意的是 select 中的 break 只能跳到 select 這一層。select 使用的時候一般配合 for 迴圈使用,像下面這樣,因為正常 select 裡面的流程也就執行一遍。這麼來看 select 中的 break 就稍顯雞肋了。所以使用 break 的時候一般配置 label 使用,label 定義在 for 迴圈這一層。
1 2 3 4 5
|
for { select { ... } }
|
3. range channel
range channel 可以直接取到 channel 中的值。當我們使用 range 來操作 channel 的時候,一旦 channel 關閉,迴圈自動結束。
1 2 3 4 5 6 7 8 9 10 11 12
|
func consumer(ch chan int) { for x := range ch { fmt.Println(x) ... } }
func producer(ch chan int) { for _, v := range values { ch <- v } }
|
4. 逾時控制
在很多操作情況下都需要逾時控制,利用 select 實現逾時控制,下面是一個簡單的樣本。
1 2 3 4 5 6
|
select { case <- ch: // get data from ch case <- time.After(2 * time.Second) // read data from ch timeout }
|
類似的,上面的 time.After 可以換成其他的任何異常控制流程。
5. 生產者-消費者模型
利用緩衝 channel 可以很輕鬆的實現生產者-消費者模型。上面的 range 樣本其實就是一個簡單的生產者-消費者模型實現。
4. 單向 channel
單向 channel,顧名思義只能寫或讀的 channel。但是仔細一想,只能寫的 channel,如果不讀其中的值有什麼用呢?其實單向 channel 主要用在函式宣告中。比如。
1
|
func foo(ch chan<- int) <-chan int {...}
|
foo 的形參是一個只能寫的 channel,那麼就表示函數 foo 只會對 ch 進行寫,當然你傳入的參數可以是個普通 channel。foo 的傳回值是一個只能讀的 channel,那麼表示 foo 的傳回值規範用法就是只能讀取。這種寫法在 Golang 的原生程式碼程式庫中有非常多的樣本,感興趣的可以去看一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
// Done returns a channel which is closed if and when this pipe is closed // with CloseWithError. func (p *http2pipe) Done() <-chan struct{} { p.mu.Lock() defer p.mu.Unlock() if p.donec == nil { p.donec = make(chan struct{}) if p.err != nil || p.breakErr != nil {
p.closeDoneLocked() } } return p.donec }
|
也許你會說這麼寫在功能上和使用普通的 channel 並不會有什麼差別。確實是這樣的。但是使用單向 channel 編程體現了一種非常優秀的編程範式:convention over configuration,中文一般叫做 約定優於配置。這種編程範式在 Ruby 中體現的尤為明顯。
5. 總結
Golang 的 channel 將 goroutine 隔離開,並發編程的時候可以將注意力放在 channel 上。在一定程度上,這個和訊息佇列的解耦功能還是挺像的。上面主要還是介紹了一些 channel 的常規操作,還有一些奇淫技巧放在參考資料裡了。之後的一篇文章還是來看看 channel 的源碼吧,對於更深入地理解 channel 還是挺有用的。
6. 參考
- Go Concurrency Patterns: Pipelines and cancellation
- Go Concurrency Visualize