【Go基礎篇】徹底搞懂 Channel 實現(xiàn)原理

最近大家私信我讓我說說 Go 語言中的 Channel,年末了水慨,有的人已經開始準備面試得糜,真快呀敬扛!今天我們就來說說 Channel嗎,日常開發(fā)中使用也是比較頻繁的朝抖,面試也是高頻啥箭。聽我慢慢說來。

Channel (通道) 是 Go 語言高性能并發(fā)編程中的核心數(shù)據(jù)結構和與 Goroutine 之前重要的通信方式治宣。在 Go 語言中通道是一種特殊的類型急侥。通道像一個傳送帶或者隊列,遵循先入先出(First In First Out)的規(guī)則侮邀,保證收發(fā)數(shù)據(jù)的順序坏怪。

1. 應用場景

在很多主流的編程語言中,多個線程間基本上都是通過共享內存來實現(xiàn)通信的绊茧,如Java铝宵。這類語言往往都需要限制一定的線程數(shù)量從而解決線程競爭。用圖的方式簡單表達一下华畏。

image

Go 語言的設計卻截然不同鹏秋,在 Go 語言提供了一種新的并發(fā)模型,在 Goroutine 中使用 Channel 傳遞數(shù)據(jù)亡笑,從而實現(xiàn)通信侣夷。

image

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ù)。如下:

image

chansend1 中調用 chansend 并傳入 channel 和需要發(fā)送的數(shù)據(jù)扛门。一開始會判斷當前 chan 是否為 nil 鸠信,是 nil 會阻塞調用者 gopark。我們會發(fā)現(xiàn)第 11 行是不會被程序執(zhí)行的论寨。

image

當 chan 關閉了星立,此時發(fā)送數(shù)據(jù)會造成 panic 錯誤。

image

如果 chan 沒有被關閉并且等待隊列中已經有處于讀等待的 Goroutine葬凳,那么會從接收隊列 recvq 中取出最先陷入等待的 Goroutine 并直接向它發(fā)送數(shù)據(jù)绰垂。

如果創(chuàng)建的 chan 包含緩沖區(qū)(chanbuf)并且 chan 中的數(shù)據(jù)沒有裝滿,會執(zhí)行下面這段代碼:

image

在這里我們首先會使用緩沖區(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。

image

可以看到 chanrecv1 和 chanrecv2 中調用 chanrecv 時 block 的值都是 true琳袄,在 chanrecv 中 chan 為 nil 江场,我們從 nil 的 chan 中接收數(shù)據(jù),調用者會被阻塞主 goroutine park窖逗,和發(fā)送一樣址否,第 15 行也不會被執(zhí)行。

image

當緩沖區(qū)中沒有數(shù)據(jù)且當前的 chan 已經 close 了,那么會清除 ep 指針中的數(shù)據(jù)佑附,代碼段會返回 true樊诺、false。

image

當緩沖區(qū)滿了音同,這個時候词爬,如果是 unbuffer 的 chan,就直接將 sender 的數(shù)據(jù)復制給 receiver权均,否則就取出隊列頭等待的 Goroutine顿膨,并把這個 sender 的值加入到隊列尾部。

3.4 關閉(close)

Go 語言中關閉一個 chan 用自帶的 close 函數(shù)叽赊,編譯器會轉成調用 closechan恋沃,我們看下源碼:

image
  • 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 關閉成功 關閉成功 關閉成功

歡迎點贊關注扯俱,公眾號搜:程序員祝融

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末书蚪,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子迅栅,更是在濱河造成了極大的恐慌殊校,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件读存,死亡現(xiàn)場離奇詭異为流,居然都是意外死亡,警方通過查閱死者的電腦和手機让簿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門敬察,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人尔当,你說我怎么就攤上這事莲祸。” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵锐帜,是天一觀的道長田盈。 經常有香客問我,道長缴阎,這世上最難降的妖魔是什么允瞧? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮药蜻,結果婚禮上瓷式,老公的妹妹穿的比我還像新娘。我一直安慰自己语泽,他們只是感情好贸典,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著踱卵,像睡著了一般廊驼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上惋砂,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天妒挎,我揣著相機與錄音,去河邊找鬼西饵。 笑死酝掩,一個胖子當著我的面吹牛,可吹牛的內容都是我干的眷柔。 我是一名探鬼主播期虾,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼驯嘱!你這毒婦竟也來了镶苞?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤鞠评,失蹤者是張志新(化名)和其女友劉穎茂蚓,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體剃幌,經...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡聋涨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了负乡。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片牛郑。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖敬鬓,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤钉答,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布础芍,位于F島的核電站,受9級特大地震影響数尿,放射性物質發(fā)生泄漏仑性。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一右蹦、第九天 我趴在偏房一處隱蔽的房頂上張望诊杆。 院中可真熱鬧,春花似錦何陆、人聲如沸晨汹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽淘这。三九已至,卻和暖如春巩剖,著一層夾襖步出監(jiān)牢的瞬間铝穷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工佳魔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留曙聂,地道東北人。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓鞠鲜,卻偏偏與公主長得像宁脊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子镊尺,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內容