0. 引言
channel 是 Go 語言中的一個非常重要的特性塞绿,這篇文章來深入了解一下 channel耳幢。
1. CSP
要想理解 channel 要先知道 CSP 模型袄友。CSP 是 Communicating Sequential Process 的簡稱挖炬,中文可以叫做通信順序進(jìn)程传泊,是一種并發(fā)編程模型,由 Tony Hoare 于 1977 年提出遵岩。簡單來說你辣,CSP 模型由并發(fā)執(zhí)行的實體(線程或者進(jìn)程)所組成座慰,實體之間通過發(fā)送消息進(jìn)行通信京郑,這里發(fā)送消息時使用的就是通道,或者叫 channel普舆。CSP 模型的關(guān)鍵是關(guān)注 channel誊锭,而不關(guān)注發(fā)送消息的實體表悬。Go 語言實現(xiàn)了 CSP 部分理論,goroutine 對應(yīng) CSP 中并發(fā)執(zhí)行的實體炉旷,channel 也就對應(yīng)著 CSP 中的 channel签孔。
2 channel 基礎(chǔ)知識
2.1 創(chuàng)建 channel
channel 使用之前需要通過 make 創(chuàng)建。
unBufferChan := make(chan int) // 1
bufferChan := make(chan int, N) // 2
上面的方式 1 創(chuàng)建的是無緩沖 channel窘行,方式 2 創(chuàng)建的是緩沖 channel饥追。如果使用 channel 之前沒有 make,會出現(xiàn) dead lock 錯誤罐盔。至于為什么是 dead lock但绕,下文我們從源碼里面看看。
func main() {
var x chan int
go func() {
x <- 1
}()
<-x
}
$ 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 讀寫操作
ch := make(chan int, 10)
// 讀操作
x <- ch
// 寫操作
ch <- x
2.3 channel 種類
channel 分為無緩沖 channel 和有緩沖 channel惶看。兩者的區(qū)別如下:
-
無緩沖:發(fā)送和接收動作是同時發(fā)生的捏顺。如果沒有 goroutine 讀取 channel (<- channel),則發(fā)送者 (channel <-) 會一直阻塞纬黎。
image -
緩沖:緩沖 channel 類似一個有容量的隊列幅骄。當(dāng)隊列滿的時候發(fā)送者會阻塞;當(dāng)隊列空的時候接收者會阻塞本今。
image
2.4 關(guān)閉 channel
channel 可以通過 built-in 函數(shù) close() 來關(guān)閉拆座。
ch := make(chan int)
// 關(guān)閉
close(ch)
關(guān)于關(guān)閉 channel 有幾點需要注意的是:
- 重復(fù)關(guān)閉 channel 會導(dǎo)致 panic主巍。
- 向關(guān)閉的 channel 發(fā)送數(shù)據(jù)會 panic。
- 從關(guān)閉的 channel 讀數(shù)據(jù)不會 panic挪凑,讀出 channel 中已有的數(shù)據(jù)之后再讀就是 channel 類似的默認(rèn)值孕索,比如 chan int 類型的 channel 關(guān)閉之后讀取到的值為 0。
對于上面的第三點躏碳,我們需要區(qū)分一下:channel 中的值是默認(rèn)值還是 channel 關(guān)閉了搞旭。可以使用 ok-idiom 方式菇绵,這種方式在 map 中比較常用肄渗。
ch := make(chan int, 10)
...
close(ch)
// ok-idiom
val, ok := <-ch
if ok == false {
// channel closed
}
3 channel 的典型用法
1 goroutine 通信
func main() {
x := make(chan int)
go func() {
x <- 1
}()
<-x
}
2 select
select 一定程度上可以類比于 linux 中的 IO 多路復(fù)用中的 select。后者相當(dāng)于提供了對多個 IO 事件的統(tǒng)一管理脸甘,而 Golang 中的 select 相當(dāng)于提供了對多個 channel 的統(tǒng)一管理恳啥。當(dāng)然這只是 select 在 channel 上的一種使用方法。
select {
case e, ok := <-ch1:
...
case e, ok := <-ch2:
...
default:
}
值得注意的是 select 中的 break 只能跳到 select 這一層丹诀。select 使用的時候一般配合 for 循環(huán)使用,像下面這樣翁垂,因為正常 select 里面的流程也就執(zhí)行一遍铆遭。這么來看 select 中的 break 就稍顯雞肋了。所以使用 break 的時候一般配置 label 使用沿猜,label 定義在 for 循環(huán)這一層枚荣。
for {
select {
...
}
}
3 range channel
range channel 可以直接取到 channel 中的值。當(dāng)我們使用 range 來操作 channel 的時候啼肩,一旦 channel 關(guān)閉橄妆,channel 內(nèi)部數(shù)據(jù)讀完之后循環(huán)自動結(jié)束。
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 實現(xiàn)超時控制害碾,下面是一個簡單的示例。
select {
case <- ch:
// get data from ch
case <- time.After(2 * time.Second)
// read data from ch timeout
}
類似的赦拘,上面的 time.After 可以換成其他的任何異郴潘妫控制流。
5 生產(chǎn)者-消費(fèi)者模型
利用緩沖 channel 可以很輕松的實現(xiàn)生產(chǎn)者-消費(fèi)者模型躺同。上面的 range 示例其實就是一個簡單的生產(chǎn)者-消費(fèi)者模型實現(xiàn)阁猜。
4 單向 channel
單向 channel,顧名思義只能寫或讀的 channel蹋艺。但是仔細(xì)一想剃袍,只能寫的 channel,如果不讀其中的值有什么用呢捎谨?其實單向 channel 主要用在函數(shù)聲明中民效。比如隘击。
func foo(ch chan<- int) <-chan int {...}
foo 的形參是一個只能寫的 channel,那么就表示函數(shù) foo 只會對 ch 進(jìn)行寫研铆,當(dāng)然你傳入的參數(shù)可以是個普通 channel埋同。foo 的返回值是一個只能讀的 channel,那么表示 foo 的返回值規(guī)范用法就是只能讀取棵红。這種寫法在 Golang 的原生代碼庫中有非常多的示例凶赁,感興趣的可以去看一下。
// 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 編程體現(xiàn)了一種非常優(yōu)秀的編程范式:convention over configuration,中文一般叫做 約定優(yōu)于配置交煞。這種編程范式在 Ruby 中體現(xiàn)的尤為明顯咏窿。
5 總結(jié)
Golang 的 channel 將 goroutine 隔離開,并發(fā)編程的時候可以將注意力放在 channel 上素征。在一定程度上集嵌,這個和消息隊列的解耦功能還是挺像的。上面主要還是介紹了一些 channel 的常規(guī)操作御毅,還有一些奇淫技巧放在參考資料里了根欧。之后的一篇文章還是來看看 channel 的源碼吧,對于更深入地理解 channel 還是挺有用的端蛆。