最近大家私信我讓我說說 Go 語言中的 Channel,年末了水慨,有的人已經開始準備面試得糜,真快呀敬扛!今天我們就來說說 Channel嗎,日常開發(fā)中使用也是比較頻繁的朝抖,面試也是高頻啥箭。聽我慢慢說來。
Channel (通道) 是 Go 語言高性能并發(fā)編程中的核心數(shù)據(jù)結構和與 Goroutine 之前重要的通信方式治宣。在 Go 語言中通道是一種特殊的類型急侥。通道像一個傳送帶或者隊列,遵循先入先出(First In First Out)的規(guī)則侮邀,保證收發(fā)數(shù)據(jù)的順序坏怪。
1. 應用場景
在很多主流的編程語言中,多個線程間基本上都是通過共享內存來實現(xiàn)通信的绊茧,如Java铝宵。這類語言往往都需要限制一定的線程數(shù)量從而解決線程競爭。用圖的方式簡單表達一下华畏。
Go 語言的設計卻截然不同鹏秋,在 Go 語言提供了一種新的并發(fā)模型,在 Goroutine 中使用 Channel 傳遞數(shù)據(jù)亡笑,從而實現(xiàn)通信侣夷。
Go 語言提倡 “不要通過共享內存的方式進行通信,而是通過 Channel 通信的方式共享內存”况芒。
Don’t communicate by sharing memory, share memory by communicating
我們會結合 chanal 使用場景的 5 大類型來闡述惜纸,更好的了解 Channel。
- 數(shù)據(jù)交流
- 數(shù)據(jù)傳遞
- 信號通知
- 任務編排
- 鎖
接下來學一下一下 chanel 的常見用法绝骚。
2. 常見用法
我們一開始就說 Go 語言是通過通信來實現(xiàn)共享內存的,故我們可以從 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ù)
聲明的通道類型變量需要使用內置的 make 函數(shù)初始化之后才能使用古瓤。格式如下:
make(chan 元素類型, [緩沖區(qū)大小])
make(chan int) // 無緩沖通道
make(chan int, 1024) // 有緩沖通道
記住 Go 語言中 chan 沒有類型的限制止剖,其中 chan 的緩沖大小是可選的,未初始化的是一個 nil 值落君。
- 指定緩沖區(qū)的大小穿香,我們稱其為 “ 緩沖通道” 。
- 未指定了緩沖區(qū)的大小绎速,我們稱其為 “ 無緩沖通道” 又稱為阻塞的通道皮获。
無緩沖通道
無緩沖的通道又稱為阻塞的通道,如上方第 3 行代碼纹冤,無緩沖的通道只有在有接收方能夠接收值的時候才能發(fā)送成功洒宝,否則會一直處于等待發(fā)送的階段购公。同理,如果對一個無緩沖通道執(zhí)行接收操作時雁歌,沒有任何向通道中發(fā)送值的操作那么也會導致接收操作阻塞宏浩。
緩沖通道
當制定了緩沖區(qū)的大小,初始化如上方第 4 行代碼靠瞎。若 chan 中有數(shù)據(jù)時比庄,此時從 chan 中接收數(shù)據(jù)不會發(fā)生阻塞;若 chan 未滿時乏盐,此時發(fā)送數(shù)據(jù)也不會發(fā)生阻塞佳窑,反之就會出現(xiàn)阻塞。
接下來說下 Channel 的基本用法丑勤。
2.1 發(fā)送數(shù)據(jù)
將一個值發(fā)送到通道(chan) 中:
chan <- 1024 // 將1024 發(fā)到 chan 中
2.2 接收數(shù)據(jù)
從一個通道(chan) 中接收值:
x := <- ch // 從ch中接收值并賦值給變量x
<-ch // 從ch中接收值华嘹,并丟棄
2.3 關閉通道
我們通過內置函數(shù) close 函數(shù)來關閉通道:
close(chan)
2.4 其他操作
Go 一些內置的函數(shù)都可以操作 chan 類型。比如 len法竞、cap
- len 可以返回 chan 中還未被處理的元素數(shù)量
- cap 可以返回 chan 的容量
注: 目前 Go 語言中并沒有提供一個不對通道進行讀取操作就能判斷通道 chan 是否被關閉的方法耙厚。不能簡單的通過 len(ch) 操作來判斷通道 chan 是否被關閉。
用 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 的具體實現(xiàn)岔霸,掌握了原理薛躬,我們才能真正地用好它,才能在談高薪是有底氣呆细!
3. 實現(xiàn)原理
3.1 chan 數(shù)據(jù)結構
Go 語言的 Channel 在運行時使用 runtime.hchan 結構體表示型宝。我們在 Go 語言中創(chuàng)建新的 Channel 時,實際上創(chuàng)建的都是如下所示的結構:
type hchan struct {
qcount uint // 循環(huán)隊列元素的數(shù)量
dataqsiz uint // 循環(huán)隊列的大小
buf unsafe.Pointer // 循環(huán)隊列緩沖區(qū)的數(shù)據(jù)指針
elemsize uint16 // chan中元素的大小
closed uint32 // 是否已close
elemtype *_type // chan 中元素類型
sendx uint // send 發(fā)送操作在 buf 中的位置
recvx uint // recv 接收操作在 buf 中的位置
recvq waitq // receiver的等待隊列
sendq waitq // senderl的等待隊列
lock mutex // 互斥鎖絮爷,保護所有字段
}
runtime.hchan
結構體中的五個字段 qcount趴酣、dataqsiz、buf坑夯、sendx岖寞、recv 構建底層的循環(huán)隊列 (channel)。解釋一下上面的字段含義:
- qcount:代表循環(huán)隊列 chan 中已經接收但還沒被取走的元素的個數(shù)柜蜈。
- datagsiz 循環(huán)隊列 chan 的大小仗谆。選用了一個循環(huán)隊列來存放元素,類似于隊列的生產者 - 消費者場景
- buf:存放元素的循環(huán)隊列的 buffer淑履。
- elemtype 和 elemsize:循環(huán)隊列 chan 中元素的類型和 size隶垮。chan 一旦聲明,它的元素類型是固定的秘噪,即普通類型或者指針類型狸吞,元素大小自然也就固定了。
- sendx:處理發(fā)送數(shù)據(jù)的指針在 buf 中的位置。一旦接收了新的數(shù)據(jù)捷绒,指針就會加上 elemsize瑰排,移向下一個位置。buf 的總大小是 elemsize 的整數(shù)倍暖侨,而且 buf 是一個循環(huán)列表椭住。
- recvx:處理接收請求時的指針在 buf 中的位置。一旦取出數(shù)據(jù)字逗,指針會移動到下一個位置京郑。
- recvq:chan 是多生產者多消費者的模式,如果消費者因為沒有數(shù)據(jù)可讀而被阻塞了葫掉,就會被加入到 recvq 隊列中些举。
- sendq:如果生產者因為 buf 滿了而阻塞,會被加入到 sendq 隊列中俭厚。
3.2 發(fā)送數(shù)據(jù)(send)
我們接下來繼續(xù)介紹 chan 的接收數(shù)據(jù)户魏。Go 語言中可以使用 ch <- i 向 chan 中發(fā)送數(shù)據(jù)。
我們看下 chansend 源碼挪挤,Go 編譯器在向 chan 發(fā)送數(shù)據(jù)時叼丑,會將 send 轉換成 chansend1 函數(shù)。如下:
chansend1 中調用 chansend 并傳入 channel 和需要發(fā)送的數(shù)據(jù)扛门。一開始會判斷當前 chan 是否為 nil 鸠信,是 nil 會阻塞調用者 gopark。我們會發(fā)現(xiàn)第 11 行是不會被程序執(zhí)行的论寨。
當 chan 關閉了星立,此時發(fā)送數(shù)據(jù)會造成 panic 錯誤。
如果 chan 沒有被關閉并且等待隊列中已經有處于讀等待的 Goroutine葬凳,那么會從接收隊列 recvq 中取出最先陷入等待的 Goroutine 并直接向它發(fā)送數(shù)據(jù)绰垂。
如果創(chuàng)建的 chan 包含緩沖區(qū)(chanbuf)并且 chan 中的數(shù)據(jù)沒有裝滿,會執(zhí)行下面這段代碼:
在這里我們首先會使用緩沖區(qū)中計算出下一個可以存儲數(shù)據(jù)的位置火焰,然后通過 typedmemmove 將發(fā)送的數(shù)據(jù)拷貝到緩沖區(qū)中并增加 sendx 索引和 qcount 計數(shù)器辕坝。
3.3 接收數(shù)據(jù)(recv)
接下來繼續(xù)介紹 chan 的接收數(shù)據(jù)。Go 語言中可以使用兩種不同的方式去接收 chan 中的數(shù)據(jù):
i <- ch
i, ok <- ch
從 chan 中接收數(shù)據(jù)會被轉換成 chanrecv1 和 chanrecv2 兩種函數(shù)荐健,但是最后還是會調用 chanrecv。
可以看到 chanrecv1 和 chanrecv2 中調用 chanrecv 時 block 的值都是 true琳袄,在 chanrecv 中 chan 為 nil 江场,我們從 nil 的 chan 中接收數(shù)據(jù),調用者會被阻塞主 goroutine park窖逗,和發(fā)送一樣址否,第 15 行也不會被執(zhí)行。
當緩沖區(qū)中沒有數(shù)據(jù)且當前的 chan 已經 close 了,那么會清除 ep 指針中的數(shù)據(jù)佑附,代碼段會返回 true樊诺、false。
當緩沖區(qū)滿了音同,這個時候词爬,如果是 unbuffer 的 chan,就直接將 sender 的數(shù)據(jù)復制給 receiver权均,否則就取出隊列頭等待的 Goroutine顿膨,并把這個 sender 的值加入到隊列尾部。
3.4 關閉(close)
Go 語言中關閉一個 chan 用自帶的 close 函數(shù)叽赊,編譯器會轉成調用 closechan恋沃,我們看下源碼:
- close 一個 nil 的 chan 會出現(xiàn) panic;
- close 一個 已經 closed 的 chan必指,會出現(xiàn) panic囊咏;
- 當 chan 不為 nil 也不為 closed,才能 close 成功塔橡,從而把等待隊列中的 sender(writer)和 receiver(reader)從隊列中全部移除并喚醒梅割。
源碼的部分就到這里了,我們接下來說下開發(fā)中需要注意的點谱邪。
4. 總結
我們開發(fā)中炮捧,chan 的值或者狀態(tài)會有很多種情況,此時一定要注意使用方式惦银,一些操作可能會出現(xiàn) panic咆课。我總結了一下異常場景,如下表:
nil channel | 有值 channel | 沒值 channel | 滿 channel | |
---|---|---|---|---|
<- ch (發(fā)送數(shù)據(jù)) | 阻塞 | 發(fā)送成功 | 發(fā)送成功 | 阻塞 |
ch <- (接收數(shù)據(jù)) | 阻塞 | 接收成功 | 阻塞 | 接收成功 |
close(ch) 關閉channel | panic | 關閉成功 | 關閉成功 | 關閉成功 |
歡迎點贊關注扯俱,公眾號搜:程序員祝融