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

最近大家私信我讓我說說 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á)一下。

image

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

image

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

image

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

image

當(dāng) chan 關(guān)閉了,此時(shí)發(fā)送數(shù)據(jù)會造成 panic 錯誤。

image

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

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

image

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

image

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

image

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

image

當(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,我們看下源碼:

image
  • 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)注,公眾號搜:程序員祝融

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,993評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內(nèi)容