Go 併發編程 - channel 連接一切
在上一篇文章中,我們介紹了 Go 併發編程的基礎—goroutine,同時也介紹 goroutine 的幾種使用方式,但沒有說明 goroutine 之間是如何通信的。
Go 語言中有一句經典的話,不要通過共享內存來通信,而應該通過通信來共享內存。這個原則讓 channel 成爲了 Go 語言中非常重要的一個組件。
goroutine 之間的通信主要是通過 channel 來完成的,這篇文章中,我們來認識一下 channel,以及 channel 的基本使用。
- 什麼是通道(channel)
Go 語言中,併發模式有兩種實現方式,一種是傳統的通過鎖和信號量等手段,來實現對個共享變量(內存)的同步訪問,從而實現併發。還有一種通過 goroutine + channel 的組合方式,傳遞值的方式來實現併發。
goroutine + channel 是對 CSP(Communicating Sequential Process)模式的一種實現。CSP 模式中,有兩個核心的概念,process 和 channel,process 對應 groutine,所有的 process 之間的通信通過 channel 來實現。
channel 是可以被單獨創建的,可以用來連接任意兩個 goroutine,channel 也有自己的數據類型,被稱之爲通道的元素類型。
創建一個通道很簡單,比如下面創建了傳遞 int 值的通道:
ch := make(chan int)
chan 表示通道,int 表示通道中傳遞的元素類型,使用 make 就可以創建一個新的通道。make 返回的結果是通道的引用,當複製這個通道或者把通道作爲函數參數的時候,傳遞的都是引用,這點很重要,需要重點理解一下。這裏順便說一下,channel 是可比較的,也就是說可以通過 == 來比較。
通道有兩個操作,一個是發送,一個是接收,都使用 <- 來表示,區別在於發送時,通道在前,接收時通道在後。向一個通道中發送數據:
x := 5
ch <- x
從通道中接收一個結果,如果不把結果賦值給一個變量,結果就會被拋棄,這樣也是合法的:
x := <-ch
<-ch // 這樣也是合法的
一個完整的發送和接收的例子如下:
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
x := 5
ch <- x
}()
y := <-ch
fmt.Println(y)
}
在使用通道的過程中,可能會出現死鎖,具體的原因我們下文再詳細說。對於通道來說,還有一個操作,就是關閉通道,對於一個已經關閉的 channel,無法再發送數據,否則會發生 panic,但是可以進行接收操作,下面的程序可以正常運行:
package main
import (
"fmt"
)
func main() {
ch := make(chan int)
go func() {
x := 5
ch <- x
close(ch)
}()
y := <-ch
fmt.Println(y)
}
- 無緩衝通道
上面用來創建通道的 make 其實還有第二個參數,用來指定通道容量。如果不指定這個參數或者指定的參數是 0,那麼就表示這個通道是無緩衝通道:
// 下面兩種創建方式是等價的
ch := make(chan int)
ch := make(chan int, 0)
在無緩衝通道上的發送操作會阻塞,直到接收端的接收操作完成,然後纔會繼續執行。在上一篇文章中,我們爲了解決主 goroutine 等待子 goroutine 執行完成用的就是這個方法。代碼如下:
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 中使用,否則會造成死鎖。關於死鎖的問題,下文再詳細討論。
- 緩衝通道
在創建緩衝通道時,需要指定通道的容量:
ch := make(chan int, 3)
上面的代碼創建了容量爲 3 的通道,可以直接向通道中發送值,發送的前 3 個操作不會阻塞:
ch <- 1
ch <- 2
ch <- 3
如果在發送的過程中,如果接收端沒有接收,那麼此時通道就是滿的,在發送第 4 個值的時候就會阻塞。
對於緩衝通道,可以使用 cap
方法得到通道的容量,可以使用 len
方法得到當前通道中元素的個數:
cap(ch) // 獲取容量
len(ch) // 獲取元素個數
對於一個緩衝通道,在同一個 goroutine 中使用也有造成死鎖的風險,所以最好不要在同一個 goroutine 中使用通道。
- 單向通道
在默認情況下,創建的通道可以發送數據,可以接受數據,但是在一些情況下,我們只需要通道的發送或者接收能力。這個時候,就需要單向通道。
單向通道的表示起來很簡單,把 <- 放在 chan 前,表示只接收,放在 chan 後表示只發送:
sendCh := male(chan<- int) // 表示只發送的通道
recCh := make(<-chan int) // 表示只接收的通道
但實際的使用中,我們不需要去創建這種單向通道,只是在某些情況下,我們把通道轉成單向通道就行。比如下面的代碼中,在 sendData 方法中,我只需要用到通道的發送能力,所以可以通道改成發送的單向通道,其他人閱讀代碼的時候,也更能理解:
func main() {
ch := make(chan int, 10)
sendData(ch)
}
func sendData(sendCh chan<- int) {
for i := 0;i < 10; i++ {
sendCh <- i
}
}
雙向通道可以轉成轉成單向通道,但反過來卻不行。
- 小結
這篇文章介紹了通道,通道對於 Go 語言來說很重要,是實現高併發的基礎,通道爲 goroutine 之間提供了一種高效安全的通信方式。但在使用通道的時候需要注意死鎖問題。
文 / Raujun
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Y1Fi96kvPnZujVDPIM3-QQ