Go - Channel

設(shè)計(jì)理念

執(zhí)行業(yè)務(wù)處理的 goroutine 不要通過(guò)共享內(nèi)存的方式通信慢逾,而是要通過(guò) Channel 通信的方式分享數(shù)據(jù)。

Channel 類(lèi)型和基本并發(fā)原語(yǔ)是有競(jìng)爭(zhēng)關(guān)系的,它應(yīng)用于并發(fā)場(chǎng)景朝卒,涉及到 goroutine 之間的通訊,可以提供并發(fā)的保護(hù)乐埠,等等抗斤。

應(yīng)用場(chǎng)景五種類(lèi)型

  1. 數(shù)據(jù)交流:當(dāng)作并發(fā)的 buffer 或者 queue,解決生產(chǎn)者 - 消費(fèi)者問(wèn)題丈咐。多個(gè) goroutine 可以并發(fā)當(dāng)作生產(chǎn)者(Producer)和消費(fèi)者(Consumer)瑞眼。
  2. 數(shù)據(jù)傳遞:一個(gè) goroutine 將數(shù)據(jù)交給另一個(gè) goroutine,相當(dāng)于把數(shù)據(jù)的擁有權(quán) (引用) 托付出去棵逊。
  3. 信號(hào)通知:一個(gè) goroutine 可以將信號(hào) (closing伤疙、closed、data ready 等) 傳遞給另一個(gè)或者另一組 goroutine 。
  4. 任務(wù)編排:可以讓一組 goroutine 按照一定的順序并發(fā)或者串行的執(zhí)行徒像,這就是編排的功能黍特。
  5. 鎖:利用 Channel 也可以實(shí)現(xiàn)互斥鎖的機(jī)制。
  6. 控制并發(fā)執(zhí)行的goroutine數(shù)量锯蛀,例如令牌桶灭衷。

基本用法

Channel 分為只能接收、只能發(fā)送旁涤、既可以接收又可以發(fā)送三種類(lèi)型翔曲。下面是它的語(yǔ)法定義:

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .

相應(yīng)地,Channel 的正確語(yǔ)法如下:

chan string          // 可以發(fā)送接收string
chan<- struct{}      // 只能發(fā)送struct{}
<-chan int           // 只能從chan接收int

通過(guò) make劈愚,我們可以初始化一個(gè) chan瞳遍,未初始化的 chan 的零值是 nil。你可以設(shè)置它的容量造虎,比如下面的 chan 的容量是 9527傅蹂,我們把這樣的 chan 叫做 buffered chan纷闺;如果沒(méi)有設(shè)置算凿,它的容量是 0,我們把這樣的 chan 叫做 unbuffered chan犁功。

make(chan int, 9527)

如果 chan 中還有數(shù)據(jù)氓轰,那么,從這個(gè) chan 接收數(shù)據(jù)的時(shí)候就不會(huì)阻塞浸卦,如果 chan 還未滿(mǎn)(“滿(mǎn)”指達(dá)到其容量)署鸡,給它發(fā)送數(shù)據(jù)也不會(huì)阻塞,否則就會(huì)阻塞限嫌。unbuffered chan 只有讀寫(xiě)都準(zhǔn)備好之后才不會(huì)阻塞靴庆,這也是很多使用 unbuffered chan 時(shí)的常見(jiàn) Bug。

nil 是 chan 的零值怒医,是一種特殊的 chan炉抒,對(duì)值是 nil 的 chan 的發(fā)送接收調(diào)用者總是會(huì)阻塞。

for + select + case + chan

func main() {
    var ch = make(chan int, 10)
    for i := 0; i < 10; i++ {
        select {
        case ch <- i:
        case v := <-ch:
            fmt.Println(v)
        }
    }
}

使用for + select循環(huán)監(jiān)控多個(gè)chan的情況稚叹,一旦發(fā)送成功 or 接收數(shù)據(jù)成功則會(huì)完成一次循環(huán)

for chan

    for v := range ch {
        fmt.Println(v)
    }

可以一直從ch中等待讀取數(shù)據(jù)焰薄,直到ch關(guān)閉才會(huì)退出循環(huán)

實(shí)現(xiàn)原理

  • qcount:代表 chan 中已經(jīng)接收但還沒(méi)被取走的元素的個(gè)數(shù)。內(nèi)建函數(shù) len 可以返回這個(gè)字段的值扒袖。- dataqsiz:隊(duì)列的大小塞茅。chan 使用一個(gè)循環(huán)隊(duì)列來(lái)存放元素,循環(huán)隊(duì)列很適合這種生產(chǎn)者 - 消費(fèi)者的場(chǎng)景(我很好奇為什么這個(gè)字段省略 size 中的 e)季率。
  • buf:存放元素的循環(huán)隊(duì)列的 buffer野瘦。
  • elemtype 和 elemsize:chan 中元素的類(lèi)型和 size。因?yàn)?chan 一旦聲明飒泻,它的元素類(lèi)型是固定的鞭光,即普通類(lèi)型或者指針類(lèi)型啊掏,所以元素大小也是固定的。
  • sendx:處理發(fā)送數(shù)據(jù)的指針在 buf 中的位置衰猛。一旦接收了新的數(shù)據(jù)迟蜜,指針就會(huì)加上 elemsize,移向下一個(gè)位置啡省。buf 的總大小是 elemsize 的整數(shù)倍娜睛,而且 buf 是一個(gè)循環(huán)列表。
  • recvx:處理接收請(qǐng)求時(shí)的指針在 buf 中的位置卦睹。一旦取出數(shù)據(jù)畦戒,此指針會(huì)移動(dòng)到下一個(gè)位置。
  • recvq:chan 是多生產(chǎn)者多消費(fèi)者的模式结序,如果消費(fèi)者因?yàn)闆](méi)有數(shù)據(jù)可讀而被阻塞了障斋,就會(huì)被加入到 recvq 隊(duì)列中。
  • sendq:如果生產(chǎn)者因?yàn)?buf 滿(mǎn)了而阻塞徐鹤,會(huì)被加入到 sendq 隊(duì)列中垃环。

send、recv返敬、close流程

本質(zhì)是 "值的拷貝"

發(fā)送操作步驟
  1. 在發(fā)送數(shù)據(jù)的邏輯執(zhí)行之前會(huì)先為當(dāng)前 Channel 加鎖遂庄,防止多個(gè)線(xiàn)程并發(fā)修改數(shù)據(jù)
  2. 如果當(dāng)前 Channel 的 recvq 上存在已經(jīng)被阻塞的 Goroutine,那么會(huì)直接將數(shù)據(jù)發(fā)送給當(dāng)前 Goroutine 并將其設(shè)置成下一個(gè)運(yùn)行的 Goroutine
  3. 如果recvq為空,則判斷緩沖區(qū)是否可寫(xiě),可寫(xiě)則從當(dāng)前goroutine復(fù)制數(shù)據(jù)到緩沖區(qū)中
  4. 如果不滿(mǎn)足上面的兩種情況劲赠,會(huì)創(chuàng)建一個(gè) runtime.sudog 結(jié)構(gòu)并將其加入 Channel 的 sendq 隊(duì)列中涛目,當(dāng)前 Goroutine 也會(huì)陷入阻塞等待其他的協(xié)程從 Channel 接收數(shù)據(jù)
  5. 寫(xiě)入完成釋放鎖.

發(fā)送數(shù)據(jù)的過(guò)程中包含幾個(gè)會(huì)觸發(fā) Goroutine 調(diào)度的時(shí)機(jī):

  1. 發(fā)送數(shù)據(jù)時(shí)發(fā)現(xiàn) Channel 上存在等待接收數(shù)據(jù)的 Goroutine,立刻設(shè)置處理器的 runnext 屬性凛澎,但是并不會(huì)立刻觸發(fā)調(diào)度霹肝;
  2. 發(fā)送數(shù)據(jù)時(shí)并沒(méi)有找到接收方并且緩沖區(qū)已經(jīng)滿(mǎn)了,這時(shí)會(huì)將自己加入 Channel 的 sendq 隊(duì)列并調(diào)用 runtime.goparkunlock 觸發(fā) Goroutine 的調(diào)度讓出處理器的使用權(quán)塑煎;
讀取/接收操作步驟
  1. 在接收數(shù)據(jù)的邏輯執(zhí)行之前會(huì)先為當(dāng)前 Channel 加鎖沫换,防止多個(gè)線(xiàn)程并發(fā)修改數(shù)據(jù);
  2. 如果 Channel 為空,那么會(huì)直接調(diào)用 runtime.gopark 掛起當(dāng)前 Goroutine;
  3. 如果 Channel 已經(jīng)關(guān)閉并且緩沖區(qū)沒(méi)有任何數(shù)據(jù)轧叽,runtime.chanrecv 會(huì)直接返回;
  4. 如果 Channel 的 sendq 隊(duì)列中存在掛起的 Goroutine, 如果不存在緩沖區(qū),則將 Channel 發(fā)送隊(duì)列中 Goroutine 存儲(chǔ)的 elem 數(shù)據(jù)拷貝到目標(biāo)內(nèi)存地址中; 如果存在緩沖區(qū)苗沧; 將隊(duì)列頭中的數(shù)據(jù)拷貝到接收方的內(nèi)存地址; 然后將發(fā)送隊(duì)列下一個(gè)的數(shù)據(jù)拷貝到緩沖區(qū)中炭晒,釋放一個(gè)阻塞的發(fā)送方待逞;
  5. 如果 Channel 的緩沖區(qū)中包含數(shù)據(jù),那么直接讀取 recvx 索引對(duì)應(yīng)的數(shù)據(jù);
  6. 在默認(rèn)情況下會(huì)掛起當(dāng)前的 Goroutine网严,將 runtime.sudog 結(jié)構(gòu)加入 recvq 隊(duì)列并陷入休眠等待調(diào)度器的喚醒;
  7. 讀取完成后釋放鎖.

總結(jié)一下從 Channel 接收數(shù)據(jù)時(shí)识樱,會(huì)觸發(fā) Goroutine 調(diào)度的兩個(gè)時(shí)機(jī):

  1. 當(dāng) Channel 為空時(shí);
  2. 當(dāng)緩沖區(qū)中不存在數(shù)據(jù)并且也不存在數(shù)據(jù)的發(fā)送者時(shí).
關(guān)閉流程
  1. 加鎖.
  2. 接著把所有掛在這個(gè) channel 上的 sender 和 receiver 全都連成一個(gè) sudog 鏈表
  3. 解鎖.
  4. 最后,再將所有的 sudog 全都喚醒.
  5. 對(duì)于等待接收者而言怜庸,會(huì)收到一個(gè)相應(yīng)類(lèi)型的零值当犯。對(duì)于等待發(fā)送者,會(huì)直接 panic割疾。所以嚎卫,在不了解 channel 還有沒(méi)有發(fā)送者的情況下,不能貿(mào)然關(guān)閉 channel. 所以最好是由唯一的channel發(fā)送者去執(zhí)行關(guān)閉操作.
關(guān)閉后

關(guān)閉流程僅僅是做一些標(biāo)記和通知操作, 實(shí)際上沒(méi)有回收掉這個(gè)chan, chan依然是可讀的, 當(dāng)讀到第二個(gè)字段為false/nil時(shí), 代表chanel已經(jīng)關(guān)閉, 通道沒(méi)有數(shù)據(jù). 對(duì)于一個(gè) channel宏榕,如果最終沒(méi)有任何 goroutine 引用它拓诸,不管 channel 有沒(méi)有被關(guān)閉,最終都會(huì)被 gc 回收

一個(gè)泄漏資源例子
func process(timeout time.Duration) bool {
    ch := make(chan bool)

    go func() {
        // 模擬處理耗時(shí)的業(yè)務(wù)
        time.Sleep((timeout + time.Second))
        ch <- true // block
        fmt.Println("exit goroutine")
    }()
    select {
    case result := <-ch:
        return result
    case <-time.After(timeout):
        return false
    }
}

在這個(gè)例子中麻昼,process 函數(shù)會(huì)啟動(dòng)一個(gè) goroutine奠支,去處理需要長(zhǎng)時(shí)間處理的業(yè)務(wù),處理完之后抚芦,會(huì)發(fā)送 true 到 chan 中倍谜,目的是通知其它等待的 goroutine,可以繼續(xù)處理了叉抡。

主 goroutine 接收到任務(wù)處理完成的通知尔崔,或者超時(shí)后就返回了。

如果發(fā)生超時(shí)卜壕,process 函數(shù)就返回了您旁,這就會(huì)導(dǎo)致 unbuffered 的 chan 從來(lái)就沒(méi)有被讀取烙常。我們知道轴捎,unbuffered chan 必須等 reader 和 writer 都準(zhǔn)備好了才能交流,否則就會(huì)阻塞蚕脏。超時(shí)導(dǎo)致未讀侦副,結(jié)果就是子 goroutine 就阻塞在第 7 行永遠(yuǎn)結(jié)束不了,進(jìn)而導(dǎo)致 goroutine 泄漏驼鞭。

解決這個(gè) Bug 的辦法很簡(jiǎn)單秦驯,就是將 unbuffered chan 改成容量為 1 的 chan,這樣第 7 行就不會(huì)被阻塞了挣棕。

chan 與 傳統(tǒng)并發(fā)原語(yǔ)選擇

  1. 共享資源的并發(fā)訪(fǎng)問(wèn)使用傳統(tǒng)并發(fā)原語(yǔ)译隘;
  2. 復(fù)雜的任務(wù)編排和消息傳遞使用 Channel;
  3. 消息通知機(jī)制使用 Channel洛心,除非只想 signal 一個(gè) goroutine固耘,才使用 Cond;
  4. 簡(jiǎn)單等待所有任務(wù)的完成用 WaitGroup词身,也有 Channel 的推崇者用 Channel厅目,都可以;
  5. 需要和 Select 語(yǔ)句結(jié)合,使用 Channel损敷;
  6. 需要和超時(shí)配合時(shí)葫笼,使用 Channel 和 Context。

panic情況匯總

文章來(lái)源

<<極客時(shí)間>>Go 并發(fā)編程實(shí)戰(zhàn)課13講

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末拗馒,一起剝皮案震驚了整個(gè)濱河市路星,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌诱桂,老刑警劉巖奥额,帶你破解...
    沈念sama閱讀 211,423評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異访诱,居然都是意外死亡垫挨,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,147評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)触菜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)九榔,“玉大人,你說(shuō)我怎么就攤上這事涡相≌懿矗” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,019評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵催蝗,是天一觀(guān)的道長(zhǎng)切威。 經(jīng)常有香客問(wèn)我,道長(zhǎng)丙号,這世上最難降的妖魔是什么先朦? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,443評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮犬缨,結(jié)果婚禮上喳魏,老公的妹妹穿的比我還像新娘。我一直安慰自己怀薛,他們只是感情好刺彩,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,535評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著枝恋,像睡著了一般创倔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上焚碌,一...
    開(kāi)封第一講書(shū)人閱讀 49,798評(píng)論 1 290
  • 那天畦攘,我揣著相機(jī)與錄音,去河邊找鬼呐能。 笑死念搬,一個(gè)胖子當(dāng)著我的面吹牛抑堡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播朗徊,決...
    沈念sama閱讀 38,941評(píng)論 3 407
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼首妖,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了爷恳?” 一聲冷哼從身側(cè)響起有缆,我...
    開(kāi)封第一講書(shū)人閱讀 37,704評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎温亲,沒(méi)想到半個(gè)月后棚壁,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,152評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡栈虚,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,494評(píng)論 2 327
  • 正文 我和宋清朗相戀三年袖外,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片魂务。...
    茶點(diǎn)故事閱讀 38,629評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡曼验,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出粘姜,到底是詐尸還是另有隱情鬓照,我是刑警寧澤,帶...
    沈念sama閱讀 34,295評(píng)論 4 329
  • 正文 年R本政府宣布孤紧,位于F島的核電站豺裆,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏号显。R本人自食惡果不足惜臭猜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,901評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望咙轩。 院中可真熱鬧获讳,春花似錦、人聲如沸活喊。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,742評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)钾菊。三九已至,卻和暖如春偎肃,著一層夾襖步出監(jiān)牢的瞬間煞烫,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,978評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工累颂, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留滞详,地道東北人凛俱。 一個(gè)月前我還...
    沈念sama閱讀 46,333評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像料饥,于是被迫代替她去往敵國(guó)和親蒲犬。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,499評(píng)論 2 348

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

  • 單純地將函數(shù)并發(fā)執(zhí)行是沒(méi)有意義的岸啡,函數(shù)與函數(shù)之間需要交換數(shù)據(jù)才能體現(xiàn)并發(fā)執(zhí)行函數(shù)的作用原叮。雖然可使用共享內(nèi)存進(jìn)行數(shù)據(jù)...
    JunChow520閱讀 419評(píng)論 0 2
  • ?? 在golang中,channel屬于較為核心的一個(gè)功能巡蘸,尤其在go協(xié)程中奋隶,channel功能尤為重要。作為g...
    北春南秋閱讀 6,022評(píng)論 2 2
  • 目錄 channel背景 channel基本用法 channel應(yīng)用場(chǎng)景 channel實(shí)現(xiàn)原理 channel數(shù)...
    邁莫coding閱讀 3,981評(píng)論 2 1
  • 了解過(guò)go的都知道悦荒,go最為突出的優(yōu)點(diǎn)就是它天然支持高并發(fā)唯欣,但是所有高并發(fā)情況都面臨著一個(gè)很明顯的問(wèn)題,就是并發(fā)的...
    GGBond_8488閱讀 329評(píng)論 0 3
  • channel一個(gè)類(lèi)型管道搬味,通過(guò)它可以在goroutine之間發(fā)送和接收消息黍聂。它是Golang在語(yǔ)言層面提供的go...
    蔡欣圻閱讀 13,473評(píng)論 4 11