
在上一篇文章中,我們介紹了 Go 并發(fā)編程的基礎(chǔ)—goroutine,同時也介紹 goroutine 的幾種使用方式,但沒有說明 goroutine 之間是如何通信的。
Go 語言中有一句經(jīng)典的話,不要通過共享內(nèi)存來通信,而應(yīng)該通過通信來共享內(nèi)存。這個原則讓 channel 成為了 Go 語言中非常重要的一個組件。
goroutine 之間的通信主要是通過 channel 來完成的,這篇文章中,我們來認(rèn)識一下 channel,以及channel 的基本使用。
1. 什么是通道(channel)
Go 語言中,并發(fā)模式有兩種實(shí)現(xiàn)方式,一種是傳統(tǒng)的通過鎖和信號量等手段,來實(shí)現(xiàn)對個共享變量(內(nèi)存)的同步訪問,從而實(shí)現(xiàn)并發(fā)。還有一種通過 goroutine + channel 的組合方式,傳遞值的方式來實(shí)現(xiàn)并發(fā)。
goroutine + channel 是對 CSP(Communicating Sequential Process)模式的一種實(shí)現(xiàn)。CSP 模式中,有兩個核心的概念,process 和 channel,process 對應(yīng) groutine,所有的 process 之間的通信通過 channel 來實(shí)現(xiàn)。
channel 是可以被單獨(dú)創(chuàng)建的,可以用來連接任意兩個 goroutine,channel 也有自己的數(shù)據(jù)類型,被稱之為通道的元素類型。
創(chuàng)建一個通道很簡單,比如下面創(chuàng)建了傳遞 int 值的通道:
ch := make(chan int)
chan 表示通道,int 表示通道中傳遞的元素類型,使用 make 就可以創(chuàng)建一個新的通道。make 返回的結(jié)果是通道的引用,當(dāng)復(fù)制這個通道或者把通道作為函數(shù)參數(shù)的時候,傳遞的都是引用,這點(diǎn)很重要,需要重點(diǎn)理解一下。這里順便說一下,channel 是可比較的,也就是說可以通過 == 來比較。
通道有兩個操作,一個是發(fā)送,一個是接收,都使用 <- 來表示,區(qū)別在于發(fā)送時,通道在前,接收時通道在后。向一個通道中發(fā)送數(shù)據(jù):
x := 5
ch <- x
從通道中接收一個結(jié)果,如果不把結(jié)果賦值給一個變量,結(jié)果就會被拋棄,這樣也是合法的:
x := <-ch
<-ch // 這樣也是合法的
一個完整的發(fā)送和接收的例子如下:
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
x := 5
ch <- x
}()
y := <-ch
fmt.Println(y)
}
在使用通道的過程中,可能會出現(xiàn)死鎖,具體的原因我們下文再詳細(xì)說。對于通道來說,還有一個操作,就是關(guān)閉通道,對于一個已經(jīng)關(guān)閉的 channel,無法再發(fā)送數(shù)據(jù),否則會發(fā)生 panic,但是可以進(jìn)行接收操作,下面的程序可以正常運(yùn)行:
package main
import (
"fmt"
)
func main() {
ch := make(chan int)
go func() {
x := 5
ch <- x
close(ch)
}()
y := <-ch
fmt.Println(y)
}
2. 無緩沖通道
上面用來創(chuàng)建通道的 make 其實(shí)還有第二個參數(shù),用來指定通道容量。如果不指定這個參數(shù)或者指定的參數(shù)是 0,那么就表示這個通道是無緩沖通道:
// 下面兩種創(chuàng)建方式是等價的
ch := make(chan int)
ch := make(chan int, 0)
在無緩沖通道上的發(fā)送操作會阻塞,直到接收端的接收操作完成,然后才會繼續(xù)執(zhí)行。在上一篇文章中,我們?yōu)榱私鉀Q主 goroutine 等待子 goroutine 執(zhí)行完成用的就是這個方法。代碼如下:
func goroutine2(isDone chan bool) {
fmt.Println("child goroutine begin...")
time.Sleep(2 * time.Second)
fmt.Println("child goroutine end...")
isDone <- true
}
func main() {
isDone := make(chan bool)
go goroutine2(isDone)
<-isDone
fmt.Println("main goroutine end..")
}
所以對于無緩沖通道來說,不能在同一個 goroutine 中使用,否則會造成死鎖。關(guān)于死鎖的問題,下文再詳細(xì)討論。
3. 緩沖通道
在創(chuàng)建緩沖通道時,需要指定通道的容量:
ch := make(chan int, 3)
上面的代碼創(chuàng)建了容量為 3 的通道,可以直接向通道中發(fā)送值,發(fā)送的前 3 個操作不會阻塞:
ch <- 1
ch <- 2
ch <- 3
如果在發(fā)送的過程中,如果接收端沒有接收,那么此時通道就是滿的,在發(fā)送第 4 個值的時候就會阻塞。
對于緩沖通道,可以使用 cap 方法得到通道的容量,可以使用 len 方法得到當(dāng)前通道中元素的個數(shù):
cap(ch) // 獲取容量
len(ch) // 獲取元素個數(shù)
對于一個緩沖通道,在同一個 goroutine 中使用也有造成死鎖的風(fēng)險,所以最好不要在同一個 goroutine 中使用通道。
4. 單向通道
在默認(rèn)情況下,創(chuàng)建的通道可以發(fā)送數(shù)據(jù),可以接受數(shù)據(jù),但是在一些情況下,我們值需要通道的發(fā)送或者接收能力。這個時候,就需要單向通道。
單向通道的表示起來很簡單,把 <- 放在 chan 前,表示只接收,放在 chan 后表示只發(fā)送:
sendCh := male(chan<- int) // 表示只發(fā)送的通道
recCh := make(<-chan int) // 表示只接收的通道
但實(shí)際的使用中,我們不需要去創(chuàng)建這種單向通道,只是在某些情況下,我們把通道轉(zhuǎn)成單向通道就行。比如下面的代碼中,在 sendData 方法中,我只需要用到通道的發(fā)送能力,所以可以通道改成發(fā)送的單向通道,其他人閱讀代碼的時候,也更能理解:
func main() {
ch := make(chan int, 10)
sendData(ch)
}
func sendData(sendCh chan<- int) {
for i := 0;i < 10; i++ {
sendCh <- i
}
}
雙向通道可以轉(zhuǎn)成轉(zhuǎn)成單向通道,但反過來卻不行。
5. 小結(jié)
這篇文章介紹了通道,通道對于 Go 語言來說很重要,是實(shí)現(xiàn)高并發(fā)的基礎(chǔ),通道為 goroutine 之間提供了一種高效安全的通信方式。但在使用通道的時候需要注意死鎖問題。
文 / Rayjun
本文首發(fā)于微信公眾號【Rayjun】