最近大家私信我讓我說說 Go 語言中的 Channel,年末了,有的人已經(jīng)開始準(zhǔn)備面試,真快呀!今天我們就來說說 Channel嗎,日常開發(fā)中使用也是比較頻繁的,面試也是高頻。聽我慢慢說來。
Channel (通道) 是 Go 語言高性能并發(fā)編程中的核心數(shù)據(jù)結(jié)構(gòu)和與 Goroutine 之前重要的通信方式。在 Go 語言中通道是一種特殊的類型。通道像一個(gè)傳送帶或者隊(duì)列,遵循先入先出(First In First Out)的規(guī)則,保證收發(fā)數(shù)據(jù)的順序。
1. 應(yīng)用場景
在很多主流的編程語言中,多個(gè)線程間基本上都是通過共享內(nèi)存來實(shí)現(xiàn)通信的,如Java。這類語言往往都需要限制一定的線程數(shù)量從而解決線程競爭。用圖的方式簡單表達(dá)一下。
Go 語言的設(shè)計(jì)卻截然不同,在 Go 語言提供了一種新的并發(fā)模型,在 Goroutine 中使用 Channel 傳遞數(shù)據(jù),從而實(shí)現(xiàn)通信。
Go 語言提倡 “不要通過共享內(nèi)存的方式進(jìn)行通信,而是通過 Channel 通信的方式共享內(nèi)存”。
Don’t communicate by sharing memory, share memory by communicating
我們會結(jié)合 chanal 使用場景的 5 大類型來闡述,更好的了解 Channel。
- 數(shù)據(jù)交流
- 數(shù)據(jù)傳遞
- 信號通知
- 任務(wù)編排
- 鎖
接下來學(xué)一下一下 chanel 的常見用法。
2. 常見用法
我們一開始就說 Go 語言是通過通信來實(shí)現(xiàn)共享內(nèi)存的,故我們可以從 channel 中接受數(shù)據(jù),也能發(fā)送數(shù)據(jù)。下文中會簡稱 channek 為 chan。我們將從一下三種情況展開說下
- 僅接送數(shù)據(jù)
- 僅發(fā)送數(shù)據(jù)
- 既能接受也能發(fā)送數(shù)據(jù)
chan int // 可以發(fā)送和接收 int 數(shù)據(jù)
chan <- struct{} // 只能發(fā)送 struct{}
<-chan string // 只能從 chan 接收 string 數(shù)據(jù)
聲明的通道類型變量需要使用內(nèi)置的 make 函數(shù)初始化之后才能使用。格式如下:
make(chan 元素類型, [緩沖區(qū)大小])
make(chan int) // 無緩沖通道
make(chan int, 1024) // 有緩沖通道
記住 Go 語言中 chan 沒有類型的限制,其中 chan 的緩沖大小是可選的,未初始化的是一個(gè) nil 值。
- 指定緩沖區(qū)的大小,我們稱其為 “ 緩沖通道” 。
- 未指定了緩沖區(qū)的大小,我們稱其為 “ 無緩沖通道” 又稱為阻塞的通道。
無緩沖通道
無緩沖的通道又稱為阻塞的通道,如上方第 3 行代碼,無緩沖的通道只有在有接收方能夠接收值的時(shí)候才能發(fā)送成功,否則會一直處于等待發(fā)送的階段。同理,如果對一個(gè)無緩沖通道執(zhí)行接收操作時(shí),沒有任何向通道中發(fā)送值的操作那么也會導(dǎo)致接收操作阻塞。
緩沖通道
當(dāng)制定了緩沖區(qū)的大小,初始化如上方第 4 行代碼。若 chan 中有數(shù)據(jù)時(shí),此時(shí)從 chan 中接收數(shù)據(jù)不會發(fā)生阻塞;若 chan 未滿時(shí),此時(shí)發(fā)送數(shù)據(jù)也不會發(fā)生阻塞,反之就會出現(xiàn)阻塞。
接下來說下 Channel 的基本用法。
2.1 發(fā)送數(shù)據(jù)
將一個(gè)值發(fā)送到通道(chan) 中:
chan <- 1024 // 將1024 發(fā)到 chan 中
2.2 接收數(shù)據(jù)
從一個(gè)通道(chan) 中接收值:
x := <- ch // 從ch中接收值并賦值給變量x
<-ch // 從ch中接收值,并丟棄
2.3 關(guān)閉通道
我們通過內(nèi)置函數(shù) close 函數(shù)來關(guān)閉通道:
close(chan)
2.4 其他操作
Go 一些內(nèi)置的函數(shù)都可以操作 chan 類型。比如 len、cap
- len 可以返回 chan 中還未被處理的元素?cái)?shù)量
- cap 可以返回 chan 的容量
注: 目前 Go 語言中并沒有提供一個(gè)不對通道進(jìn)行讀取操作就能判斷通道 chan 是否被關(guān)閉的方法。不能簡單的通過 len(ch) 操作來判斷通道 chan 是否被關(guān)閉。
用 for range 接收值:
func f(ch chan int) {
for v := range ch {
fmt.Println("接收到 chan 值:", v)
}
}
還有 send 和 recv 可以作為 select 語句的 case:
var ch = make(chan int, 6)
for i := 0; i < 6; i++ {
select {
case ch <- i:
case v := <-ch:
fmt.Println(v)
}
}
下面我說下源碼的角度分析一下 chan 的具體實(shí)現(xiàn),掌握了原理,我們才能真正地用好它,才能在談高薪是有底氣!
3. 實(shí)現(xiàn)原理
3.1 chan 數(shù)據(jù)結(jié)構(gòu)
Go 語言的 Channel 在運(yùn)行時(shí)使用 runtime.hchan 結(jié)構(gòu)體表示。我們在 Go 語言中創(chuàng)建新的 Channel 時(shí),實(shí)際上創(chuàng)建的都是如下所示的結(jié)構(gòu):
type hchan struct {
qcount uint // 循環(huán)隊(duì)列元素的數(shù)量
dataqsiz uint // 循環(huán)隊(duì)列的大小
buf unsafe.Pointer // 循環(huán)隊(duì)列緩沖區(qū)的數(shù)據(jù)指針
elemsize uint16 // chan中元素的大小
closed uint32 // 是否已close
elemtype *_type // chan 中元素類型
sendx uint // send 發(fā)送操作在 buf 中的位置
recvx uint // recv 接收操作在 buf 中的位置
recvq waitq // receiver的等待隊(duì)列
sendq waitq // senderl的等待隊(duì)列
lock mutex // 互斥鎖,保護(hù)所有字段
}
runtime.hchan
結(jié)構(gòu)體中的五個(gè)字段 qcount、dataqsiz、buf、sendx、recv 構(gòu)建底層的循環(huán)隊(duì)列 (channel)。解釋一下上面的字段含義:
- qcount:代表循環(huán)隊(duì)列 chan 中已經(jīng)接收但還沒被取走的元素的個(gè)數(shù)。
- datagsiz 循環(huán)隊(duì)列 chan 的大小。選用了一個(gè)循環(huán)隊(duì)列來存放元素,類似于隊(duì)列的生產(chǎn)者 - 消費(fèi)者場景
- buf:存放元素的循環(huán)隊(duì)列的 buffer。
- elemtype 和 elemsize:循環(huán)隊(duì)列 chan 中元素的類型和 size。chan 一旦聲明,它的元素類型是固定的,即普通類型或者指針類型,元素大小自然也就固定了。
- sendx:處理發(fā)送數(shù)據(jù)的指針在 buf 中的位置。一旦接收了新的數(shù)據(jù),指針就會加上 elemsize,移向下一個(gè)位置。buf 的總大小是 elemsize 的整數(shù)倍,而且 buf 是一個(gè)循環(huán)列表。
- recvx:處理接收請求時(shí)的指針在 buf 中的位置。一旦取出數(shù)據(jù),指針會移動到下一個(gè)位置。
- recvq:chan 是多生產(chǎn)者多消費(fèi)者的模式,如果消費(fèi)者因?yàn)闆]有數(shù)據(jù)可讀而被阻塞了,就會被加入到 recvq 隊(duì)列中。
- sendq:如果生產(chǎn)者因?yàn)?buf 滿了而阻塞,會被加入到 sendq 隊(duì)列中。
3.2 發(fā)送數(shù)據(jù)(send)
我們接下來繼續(xù)介紹 chan 的接收數(shù)據(jù)。Go 語言中可以使用 ch <- i 向 chan 中發(fā)送數(shù)據(jù)。
我們看下 chansend 源碼,Go 編譯器在向 chan 發(fā)送數(shù)據(jù)時(shí),會將 send 轉(zhuǎn)換成 chansend1 函數(shù)。如下:
chansend1 中調(diào)用 chansend 并傳入 channel 和需要發(fā)送的數(shù)據(jù)。一開始會判斷當(dāng)前 chan 是否為 nil ,是 nil 會阻塞調(diào)用者 gopark。我們會發(fā)現(xiàn)第 11 行是不會被程序執(zhí)行的。
當(dāng) chan 關(guān)閉了,此時(shí)發(fā)送數(shù)據(jù)會造成 panic 錯誤。
如果 chan 沒有被關(guān)閉并且等待隊(duì)列中已經(jīng)有處于讀等待的 Goroutine,那么會從接收隊(duì)列 recvq 中取出最先陷入等待的 Goroutine 并直接向它發(fā)送數(shù)據(jù)。
如果創(chuàng)建的 chan 包含緩沖區(qū)(chanbuf)并且 chan 中的數(shù)據(jù)沒有裝滿,會執(zhí)行下面這段代碼:
在這里我們首先會使用緩沖區(qū)中計(jì)算出下一個(gè)可以存儲數(shù)據(jù)的位置,然后通過 typedmemmove 將發(fā)送的數(shù)據(jù)拷貝到緩沖區(qū)中并增加 sendx 索引和 qcount 計(jì)數(shù)器。
3.3 接收數(shù)據(jù)(recv)
接下來繼續(xù)介紹 chan 的接收數(shù)據(jù)。Go 語言中可以使用兩種不同的方式去接收 chan 中的數(shù)據(jù):
i <- ch
i, ok <- ch
從 chan 中接收數(shù)據(jù)會被轉(zhuǎn)換成 chanrecv1 和 chanrecv2 兩種函數(shù),但是最后還是會調(diào)用 chanrecv。
可以看到 chanrecv1 和 chanrecv2 中調(diào)用 chanrecv 時(shí) block 的值都是 true,在 chanrecv 中 chan 為 nil ,我們從 nil 的 chan 中接收數(shù)據(jù),調(diào)用者會被阻塞主 goroutine park,和發(fā)送一樣,第 15 行也不會被執(zhí)行。
當(dāng)緩沖區(qū)中沒有數(shù)據(jù)且當(dāng)前的 chan 已經(jīng) close 了,那么會清除 ep 指針中的數(shù)據(jù),代碼段會返回 true、false。
當(dāng)緩沖區(qū)滿了,這個(gè)時(shí)候,如果是 unbuffer 的 chan,就直接將 sender 的數(shù)據(jù)復(fù)制給 receiver,否則就取出隊(duì)列頭等待的 Goroutine,并把這個(gè) sender 的值加入到隊(duì)列尾部。
3.4 關(guān)閉(close)
Go 語言中關(guān)閉一個(gè) chan 用自帶的 close 函數(shù),編譯器會轉(zhuǎn)成調(diào)用 closechan,我們看下源碼:
- close 一個(gè) nil 的 chan 會出現(xiàn) panic;
- close 一個(gè) 已經(jīng) closed 的 chan,會出現(xiàn) panic;
- 當(dāng) chan 不為 nil 也不為 closed,才能 close 成功,從而把等待隊(duì)列中的 sender(writer)和 receiver(reader)從隊(duì)列中全部移除并喚醒。
源碼的部分就到這里了,我們接下來說下開發(fā)中需要注意的點(diǎn)。
4. 總結(jié)
我們開發(fā)中,chan 的值或者狀態(tài)會有很多種情況,此時(shí)一定要注意使用方式,一些操作可能會出現(xiàn) panic。我總結(jié)了一下異常場景,如下表:
nil channel | 有值 channel | 沒值 channel | 滿 channel | |
---|---|---|---|---|
<- ch (發(fā)送數(shù)據(jù)) | 阻塞 | 發(fā)送成功 | 發(fā)送成功 | 阻塞 |
ch <- (接收數(shù)據(jù)) | 阻塞 | 接收成功 | 阻塞 | 接收成功 |
close(ch) 關(guān)閉channel | panic | 關(guān)閉成功 | 關(guān)閉成功 | 關(guān)閉成功 |
歡迎點(diǎn)贊關(guān)注,公眾號搜:程序員祝融