Golang channel 之 讀操作 recv

上一篇:Golang channel 之 寫操作 send

channel的常規(guī)讀操作

假如有一個(gè)元素類型為int的channel,變量名為ch,那么常規(guī)的讀操作(簡稱recv為讀)在代碼中的寫法如下所示:

// 將結(jié)果丟棄
<-ch
// 將結(jié)果賦值給變量v
v := <-ch
// comma ok style,ok為false表示ch已關(guān)閉且v不是“讀出來的”
v, ok := <-ch

其中ch可能是“有緩沖”的,也可能是“無緩沖”的,甚至可能為nil。

按照上面的寫法,有兩種情況能使讀操作不會(huì)阻塞:
1)通道ch的sendq里已有g(shù)oroutine在等待;
2)通道ch的sendq是空的,但是通道“有緩沖”且緩沖區(qū)中有數(shù)據(jù)。

第一種情況中,只要ch的sendq里有協(xié)程在排隊(duì),那么需要進(jìn)一步判斷通道是否“有緩沖”:
如果“無緩沖”,當(dāng)前協(xié)程就直接從sendq隊(duì)首的那個(gè)協(xié)程那里拿過數(shù)據(jù),然后兩者都可以繼續(xù)執(zhí)行;
如果“有緩沖”,隱含信息就是緩沖區(qū)已滿,否則sendq中不會(huì)有協(xié)程排隊(duì),這時(shí)當(dāng)前協(xié)程從緩沖區(qū)取出第一個(gè)數(shù)據(jù)(緩沖區(qū)有了一個(gè)空閑位置),然后從sendq中取出第一個(gè)協(xié)程,把它的數(shù)據(jù)追加到緩沖區(qū)中,并把它置成ready狀態(tài),最終兩個(gè)協(xié)程都能繼續(xù)執(zhí)行。

第二種情況中,ch的sendq里沒有協(xié)程在排隊(duì),所以不需要關(guān)心。ch是有緩沖的,且緩沖區(qū)有數(shù)據(jù),那么當(dāng)前協(xié)程直接從緩沖區(qū)取出第一個(gè)數(shù)據(jù),然后就可以繼續(xù)執(zhí)行了。

同樣是上面的寫法,有三種情況會(huì)使讀操作阻塞:
1)通道ch為nil;
2)通道ch無緩沖且sendq為空;
3)通道ch有緩沖且緩沖區(qū)無數(shù)據(jù)。

第一種情況中,參照golang的實(shí)現(xiàn),允許對(duì)nil通道執(zhí)行讀操作,但是會(huì)使當(dāng)前協(xié)程永久性的阻塞在這個(gè)nil通道上,例如如下代碼會(huì)因死鎖拋出異常:

package main

func main() {
        var ch chan int
        <-ch
}

第二種情況中,ch為無緩沖通道,sendq中沒有協(xié)程在等待,所以當(dāng)前協(xié)程需要到通道的recvq中排隊(duì);

第三種情況中,ch有緩沖但是沒有數(shù)據(jù),隱含的信息就是sendq為空,否則緩沖區(qū)不可能沒有數(shù)據(jù),所以當(dāng)前協(xié)程只能到recvq中排隊(duì)。

channel的非阻塞讀操作

還是類似tryLock操作:我想獲得這把鎖,但是萬一已經(jīng)被別人獲得了,我不阻塞等待,可以去干其他事情。

對(duì)于通道的非阻塞讀就是:我想從通道讀取數(shù)據(jù),但是當(dāng)前沒有寫者在排隊(duì)等待,且緩沖區(qū)內(nèi)無數(shù)據(jù)(包含無緩沖),我就需要阻塞等待。但是我不想等待,所以立刻返回并告訴我“現(xiàn)在無數(shù)據(jù)”就可以了。

在golang中,對(duì)于單個(gè)通道的非阻塞讀操作可以用如下代碼實(shí)現(xiàn),注意是一個(gè)select、一個(gè)case和一個(gè)default,都是一個(gè),不能多也不能少:

select {
case <-ch: // 此處可以帶有賦值操作,或者是comma ok style
    ...
default:
    ...
}

如果檢測(cè)到讀ch不會(huì)阻塞,那么就會(huì)執(zhí)行case <-ch:分支,如果會(huì)阻塞,就會(huì)執(zhí)行default:分支。關(guān)于什么情況下會(huì)阻塞,什么情況下不會(huì)阻塞,參見上面的情況分析。

channel讀操作的實(shí)現(xiàn)

上面簡單的分析了channel的常規(guī)讀操作和非阻塞讀操作,雖然兩者在形式上看起來稍微有些差異,但是主要邏輯都是通過runtime.chanrecv函數(shù)實(shí)現(xiàn)的,下面簡單的進(jìn)行一下解讀:

首先來看一下chanrecv函數(shù)的原型:

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)

其中:
c是一個(gè)hchan指針,指向要從中recv數(shù)據(jù)的channel;
ep是一個(gè)指針,指向用來接收數(shù)據(jù)的內(nèi)存,數(shù)據(jù)類型要和c的元素類型一致;
block表示如果讀操作不能立即完成,是否想要阻塞等待;
selected為true表示操作完成(可能因?yàn)橥ǖ酪殃P(guān)閉),false表示目前不可讀但因?yàn)椴幌胱枞╞lock為false)而返回;
received為true表示數(shù)據(jù)確實(shí)是從通道中讀出來的,不是因?yàn)橥ǖ狸P(guān)閉而得到的零值,為false的情況需要結(jié)合selected來解釋,可能是因?yàn)橥ǖ狸P(guān)閉而得到零值(selected為true),或者因?yàn)椴幌胱枞祷兀╯elected為false)。

chanrecv函數(shù)的主要邏輯如下:

如果c等于nil {
    如果不想阻塞 {
        return false, false
    }
    永久阻塞
}

如果不想阻塞 且 ((c無緩沖 且 sendq是空的) 或 (c有緩沖 且 緩沖區(qū)為空)) 且 c未關(guān)閉 {
    return false, false
}

對(duì)c加鎖

如果c已關(guān)閉 且 緩沖區(qū)為空 {
    解鎖c
    為ep賦零值
    return true, false
}

如果sendq中有內(nèi)容 {
    取出隊(duì)首協(xié)程sg
    如果有緩沖 {
        取出緩沖區(qū)第一個(gè)數(shù)據(jù)賦給ep,再把sg的數(shù)據(jù)追加到緩沖區(qū),調(diào)整sendx、recvx
    } 否則 {
        把sg的數(shù)據(jù)賦給ep
    }
    解鎖c,將sg置為ready
    return true, true
}

如果緩沖區(qū)有數(shù)據(jù) {
    取出第一個(gè)數(shù)據(jù)賦給ep,移動(dòng)recvx,遞減qcount
    解鎖c
    return true, true
}

如果不想阻塞 {
    解鎖c
    return false, false
}

進(jìn)入recvq排隊(duì)等待同時(shí)解鎖c,條件滿足時(shí)會(huì)完成數(shù)據(jù)讀取并被喚醒

return true, 完成數(shù)據(jù)讀取時(shí)通道沒有關(guān)閉

逐塊對(duì)應(yīng)以上代碼:
1)如果c為nil,進(jìn)一步判斷block:如果block為false,那么直接返回兩個(gè)false,表示未recv數(shù)據(jù);如果block為true,那么就讓當(dāng)前協(xié)程”永久“的阻塞在這個(gè)nil通道上;
2)如果block為false,也就是在”不想阻塞“的前提下,如果是通道”無緩沖“且sendq為空,或者是通道”有緩沖“且緩沖區(qū)為空,最后再判斷通道未關(guān)閉的話,則直接返回兩個(gè)false。本步判斷是在不加鎖的情況下進(jìn)行的,目的是讓非阻塞讀在無法立即完成時(shí)能真正不阻塞(加鎖可能阻塞)。這幾步判斷使用了atomic函數(shù),并且先后順序不能打亂,要在最后一步判斷通道未關(guān)閉。因?yàn)殛P(guān)閉后的通道不能再被打開,這樣保證了并發(fā)條件下的一致性,如果把判斷closed前置,則在檢查緩沖區(qū)和sendq時(shí)通道可能已關(guān)閉,這樣會(huì)出現(xiàn)錯(cuò)誤;
3)加鎖;
4)如果closed不為0,即通道已經(jīng)關(guān)閉的話,則解鎖,然后給ep賦零值,返回true和false;
5)如果sendq不為空,就從中取出第一個(gè)排隊(duì)的協(xié)程sg,如果有緩沖則還需要滾動(dòng)緩沖區(qū),完成數(shù)據(jù)讀取,并將協(xié)程sg置為ready狀態(tài)(放入run queue,進(jìn)而得到調(diào)度),然后解鎖,返回兩個(gè)true;
6)通過qcount判斷緩沖區(qū)是否有數(shù)據(jù),在這里”無緩沖“的通道被視為沒有數(shù)據(jù),到達(dá)這一步sendq一定為空所以不必關(guān)心。緩沖區(qū)有數(shù)據(jù)的話,將第一個(gè)數(shù)據(jù)取出并賦給ep,移動(dòng)recvx,遞減qcount,解鎖,返回兩個(gè)true;
7)如果block為false,即不想阻塞,則解鎖,返回兩個(gè)false;
8)最后,到達(dá)這里就是阻塞讀了,當(dāng)前協(xié)程把自己追加到通道的recvq中阻塞排隊(duì),同時(shí)解鎖,等到條件滿足時(shí)會(huì)被喚醒。
9)被喚醒有可能是因?yàn)橥ǖ辣魂P(guān)閉,所以最后的返回值received需要根據(jù)被喚醒的原因來判斷,若是因?yàn)榈鹊秸鎸?shí)數(shù)據(jù)則為true,若是因?yàn)橥ǖ狸P(guān)閉則為false。

本篇總結(jié)

1)channel的常規(guī)讀操作如v := <-c,會(huì)被編譯器轉(zhuǎn)換為對(duì)runtime.chanrecv1的調(diào)用,后者內(nèi)部只是調(diào)用了runtime.chanrecv,comma ok寫法會(huì)被編譯器轉(zhuǎn)換為對(duì)runtime.chanrecv2的調(diào)用,與chanrecv1的唯一區(qū)別就是把received返回值賦給了ok;
2)非阻塞式的讀操作,即select、case、default三個(gè)一,會(huì)被編譯器轉(zhuǎn)換為對(duì)runtime.selectnbrecv或selectnbrecv2的調(diào)用(根據(jù)是否comma ok),后兩者也僅僅是調(diào)用了runtime.chanrecv。非阻塞讀的實(shí)現(xiàn)效果如下:

select {
case v = <-c:
    ... foo
default:
    ... bar
}

// 被編譯器轉(zhuǎn)化為

if selectnbrecv(&v, c) {
    ... foo
} else {
    ... bar
}

comma ok寫法:

select {
case v, ok = <-c:
    ... foo
default:
    ... bar
}

// 被編譯器轉(zhuǎn)化為

if c != nil && selectnbrecv2(&v, &ok, c) {
    ... foo
} else {
    ... bar
}

上一篇:Golang channel 之 寫操作 send

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容