golang之channel

前言

本文算是對Diving Deep Into The Golang Channels的翻譯,也算加強對channel的了解和使用笋熬。

使用channel

func goRoutineA(a <- chan int){
  val := <- a
  fmt.Println("goRoutineA received the data", val)
}

func main(){
  ch := make(chan int) // 定義一個channel:接收int類型
  go goRoutineA(ch)
  time.Sleep(time.Second * 1)  // 防止主線程退出看不到goroutine內(nèi)容輸出
}

整個執(zhí)行流程如下


channel-1

channel-2

從上面兩張圖片看到:使用make(chan int)定義的channel较锡,當(dāng)channel中不存在數(shù)據(jù)時 在執(zhí)行<- a時會被blocked直到channel中有數(shù)據(jù)点楼。
在golang中使用channel能夠使得runnable的goroutine在向channel發(fā)送或接收數(shù)據(jù)時處于blocked宠纯。

channel structure

在go中斗躏,channel實現(xiàn)了在不同的goroutine間傳遞message的基本。
當(dāng)我們使用make函數(shù)來創(chuàng)建channel后衬浑,對應(yīng)的結(jié)構(gòu)應(yīng)該是怎樣的捌浩?

ch := make(chan int, 3)
在runtime時channel具體結(jié)構(gòu)

接下來我們會針對其中的一些內(nèi)容進(jìn)行詳解

hchan struct

當(dāng)我們通過make(chan int, 3)創(chuàng)建一個buffer=3的channel時,就會創(chuàng)建一個hchan結(jié)構(gòu)


hchan
  • dataqsize: 對應(yīng)的channel的buffer大小工秩,比如使用make(chan T, N),其中T代表channel中元素的類型尸饺,N就是channel的buffer大小助币;
  • elementsize:channel中的元素大欣颂;
  • buf:channel中element真正存放的循環(huán)隊列眉菱;不過該字段只有在使用buffered的channel時才有意義迹栓;
  • closed:記錄當(dāng)前channel是否已關(guān)閉,在使用make創(chuàng)建一個channel后俭缓,closed=0克伊, 代表當(dāng)前channel處于open;當(dāng)調(diào)用close時可將該channel關(guān)閉华坦,closed=1愿吹;代表當(dāng)前channel不能再進(jìn)行任何write操作。
  • recvq 和 sendq:都是等待隊列惜姐,主要存放進(jìn)行讀取channel數(shù)據(jù)或?qū)懭隿hannel數(shù)據(jù)時處于blocked的goroutines洗搂。
  • lock:主要用來保證channel的讀寫或發(fā)送接收是互斥操作。確保對channel的讀取或?qū)懭氲淖枞?/li>

sudog struct

可將sudgo當(dāng)成goroutine來理解


sudog結(jié)構(gòu)

先將前面的實例進(jìn)行調(diào)整下:

func goRoutineA(a <- chan int){
  val := <- a
  fmt.Println("goroutineA received the data", val)
}

func goRoutineB(b <- chan int){
  val := <- b
  fmt.Println("goroutineB received the data ", val)
}

func main(){
  ch := make(chan int)
  go goRoutineA(ch)
  go goRoutineB(ch)
   ch <- 3  
  time.Sleep(time.Second * 1)
}

對應(yīng)的生成的channel結(jié)構(gòu)如下:


channel結(jié)構(gòu)

可以看到凸出部分展示了本實例中定義兩個goroutine(goroutineA和goroutineB)來嘗試讀取channel中的數(shù)據(jù)载弄。在執(zhí)行 ch <- 3之前,由于channel中并沒有任何數(shù)據(jù)撵颊,而兩個goroutine將會阻塞在接收數(shù)據(jù)操作上宇攻,并用sudog進(jìn)行包裝,同時兩個sudog會被存放到recvq里倡勇。
在channel中的recvq和sendq都是基于鏈表實現(xiàn)的逞刷,如下


channel之recvq

對于channel的sendq類似,此處不再累述妻熊。接下來看看當(dāng)執(zhí)行ch <-3發(fā)生了什么夸浅?

channel之send操作: c<- x

先看看如下幾種send操作:

  • 1.對nil channel執(zhí)行send操作
nil channel執(zhí)行send

在對一個nil channel執(zhí)行send操作時 會導(dǎo)致當(dāng)前goroutine暫停其操作

  • 2.對closed channel執(zhí)行send
closed channel

向一個已經(jīng)closed的channel發(fā)送數(shù)據(jù)會觸發(fā)一個panic

  • 3.當(dāng)一個goroutine阻塞在channel上,send數(shù)據(jù)時會直接將數(shù)據(jù)發(fā)送該goroutine
blocked channel

該實例也說明recvq在其中扮演一個最終的角色:若是在recvq中任意一個goroutine在等待接收數(shù)據(jù)扔役,對應(yīng)的channel的wirter會直接將value傳遞給當(dāng)前的goroutine(waiting receiver)帆喇。見send函數(shù):


channel之send

在396行代碼處 goready(gp, skip + 1),會使得在阻塞等待數(shù)據(jù)的那個goroutine將被再次runnable,go scheduler也將會再次運行該goroutine亿胸。

  • 4.Buffered Channel

當(dāng)我們通過make(chan T, N)定義一個帶有buffer的channel時坯钦,若是對應(yīng)的hchan.buf還有可用空間則會將data存到到buffer中而不是像非buffered的channel一樣處于阻塞预皇,等待數(shù)據(jù)被接收。


buffered channel

chanbuf(c, i)直接訪問相應(yīng)的內(nèi)存空間婉刀。
通過對比qcount和dataqsiz來判斷hchan.buf是否還有free空間吟温;通過將ep指針指向的區(qū)域copy到ringbuffer,來完成入列元素的send操作突颊,并調(diào)整sendx和qcount鲁豪。

  • 5.若是hchan.buf沒有可用空間時 會如何?律秃?爬橡?
full channel

上述代碼:
首先會在當(dāng)前stack上創(chuàng)建一個goroutine,并將該goroutine狀態(tài)=park同時將該goroutine添加到sendq中友绝。

關(guān)于send

1.將當(dāng)前的channel進(jìn)行blocked
2.確定執(zhí)行write堤尾,會從recvq中獲取一個等待的goroutine,并將對應(yīng)的element直接寫給該goroutine迁客。
3.當(dāng)對應(yīng)的recvq是空的郭宝,首先要確保當(dāng)前的buffer是否可用,若是可用掷漱,則從當(dāng)前的goroutine的copy數(shù)據(jù)到buffer中
typedmemmove內(nèi)部使用memmove將一個內(nèi)存塊從一個位置copy到另外一個位置粘室。
4.若是buffer已滿,則寫入到channel的元素會被保存到當(dāng)前運行的goroutine卜范,并且當(dāng)前goroutine將sendq處進(jìn)行等待衔统。
通過對比buffered channel和unbuffered channel差別在于對應(yīng)的hchan分配有buffer。對于一個unbuffered channel 當(dāng)send數(shù)據(jù)時并沒有對應(yīng)的receiver則會將元素保存到sudog中的elem字段海雪,對應(yīng)buffered channel也是同樣的道理锦爵。
接下來會通過結(jié)合實例來闡述關(guān)于上面羅列的第4點:
如下代碼只是用來演示 執(zhí)行可能會導(dǎo)致一個panic

package main

func goroutineA(c2 chan int){
  c2 <- 2
}

func main(){
  c2 := make(chan int)
  go routineA(c2)

  for{}
}

如上的運行時channel的結(jié)構(gòu)


unbuffered

不過即使我們將值2添加到channel中對應(yīng)的buf卻不存在該值,將會保存在goroutine的sudog結(jié)構(gòu)中奥裸。在上面例子中g(shù)oroutineA向channel c2發(fā)送數(shù)據(jù)险掀,但此時并沒有對應(yīng)的receiver準(zhǔn)備接收數(shù)據(jù),因而goroutineA將被添加到channel的sendq列表中湾宙,并一直阻塞暫停等待receiver來獲取數(shù)據(jù)樟氢。接下來看看運行時的sendq結(jié)構(gòu),來驗證前面的內(nèi)容


runtime sendq

這樣在實例代碼中 ch <- 2后具體發(fā)生的事宜侠鳄。
而對于recvq來說如果存在等待狀態(tài)的goroutine埠啃,它獲取queue的第一個sudog并將數(shù)據(jù)放到goroutine中。

針對channel所有的transfer都是采用值copy的方式伟恶。也就是說在channel的所有的操作都是值拷貝碴开。


值拷貝

正如上面演示樣例 也是通過拷貝g的值到buffer中。
Don't communicate by sharing memory; share memory by communicating.

&{Ankur 25}
modifyUser Received Value &{Ankur Anand 100}
printUser goRoutine called &{Ankur 25}
&{Anand 100}
樣例值拷貝

receive channel

其實跟channel send操作很類似博秫。


channel receiver

Select: 多路復(fù)用

演示實例


multiplexing on multiple channel

1.在select代碼塊中的case執(zhí)行都是互斥的叹螟,故而是需要select case中的channel來獲取lock執(zhí)行的鹃骂,每個channel獲取執(zhí)行l(wèi)ock的順序是基于Hchan地址的排序來進(jìn)行l(wèi)ock的獲取,這樣就能確保不會同時鎖定所有相關(guān)通道的互斥鎖罢绽。

sellock(scases, lockorder)

每個在scases數(shù)組中的scase包括當(dāng)前case的操作類型以及它所在的channel畏线。


scase結(jié)構(gòu)
  • kind 代表當(dāng)前case的操作類型,可能取值:CaseRecv良价、CaseSend寝殴、CaseDefault
    2.計算輪詢順序:shuffle所有涉及的通道,以提供偽隨機保證明垢,并根據(jù)輪詢順序依次遍歷所有情況蚣常,以查看其中是否有準(zhǔn)備好進(jìn)行通信。這個輪詢順序使得select操作不必遵循程序中聲明的順序痊银。


    poll order

    case in select

    3.在select代碼塊中抵蚊,只要有一個通道操作沒有阻塞,select語句就可以返回溯革,如果選擇的通道已經(jīng)準(zhǔn)備好了贞绳,甚至不需要接觸所有通道。
    若是當(dāng)前沒有通道響應(yīng)致稀,也沒有默認(rèn)語句冈闭,則當(dāng)前g必須根據(jù)情況掛載所有通道的相應(yīng)等待隊列。
    若是當(dāng)前所有的case都已準(zhǔn)備好抖单, 則會隨機執(zhí)行一個case萎攒。


    park goroutine in select case
  • sg.isSelect 代表goroutine正在參與當(dāng)前的select塊。

channel是go中一個非常強大和有趣的機制矛绘。但是為了有效地使用它們耍休,你必須了解它們是如何工作的。希望本文能夠解釋Go中通道所涉及的非郴醢基本的工作原理羊精。

最后推薦Go Study Group 歡迎加入。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末次屠,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子雳刺,更是在濱河造成了極大的恐慌劫灶,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掖桦,死亡現(xiàn)場離奇詭異本昏,居然都是意外死亡,警方通過查閱死者的電腦和手機枪汪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門涌穆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來怔昨,“玉大人,你說我怎么就攤上這事宿稀〕靡ǎ” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵祝沸,是天一觀的道長矮烹。 經(jīng)常有香客問我,道長罩锐,這世上最難降的妖魔是什么奉狈? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮涩惑,結(jié)果婚禮上仁期,老公的妹妹穿的比我還像新娘。我一直安慰自己竭恬,他們只是感情好跛蛋,可當(dāng)我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著萍聊,像睡著了一般问芬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上寿桨,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天此衅,我揣著相機與錄音,去河邊找鬼亭螟。 笑死挡鞍,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的预烙。 我是一名探鬼主播墨微,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼扁掸!你這毒婦竟也來了翘县?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤谴分,失蹤者是張志新(化名)和其女友劉穎锈麸,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體牺蹄,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡忘伞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片氓奈。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡翘魄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出舀奶,到底是詐尸還是另有隱情暑竟,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布伪节,位于F島的核電站光羞,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏怀大。R本人自食惡果不足惜纱兑,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望化借。 院中可真熱鬧潜慎,春花似錦、人聲如沸蓖康。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蒜焊。三九已至倒信,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間泳梆,已是汗流浹背鳖悠。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留优妙,地道東北人乘综。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像套硼,于是被迫代替她去往敵國和親卡辰。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,802評論 2 345

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