圖解Go select語句原理

Go 的select語句是一種僅能用於channl發送和接收消息的專用語句,此語句運行期間是阻塞的;當select中沒有case語句的時候,會阻塞當前的groutine。所以,有人也會說select是用來阻塞監聽goroutine的。
還有人說:select是Golang在語言層面提供的I/O多路複用的機制,其專門用來檢測多個channel是否準備完畢:可讀或可寫。

以上說法都正確。

I/O多路複用

我們來回顧一下是什麼是I/O多路複用

普通多線程(或進程)I/O

每來一個進程,都會建立連接,然後阻塞,直到接收到數據返回響應。
普通這種方式的缺點其實很明顯:系統需要創建和維護額外的線程或進程。因爲大多數時候,大部分阻塞的線程或進程是處於等待狀態,只有少部分會接收並處理響應,而其餘的都在等待。系統爲此還需要多做很多額外的線程或者進程的管理工作。

爲了解決圖中這些多餘的線程或者進程,於是有了"I/O多路複用"

I/O多路複用

每個線程或者進程都先到圖中”裝置“中註冊,然後阻塞,然後只有一個線程在”運輸“,當註冊的線程或者進程準備好數據後,”裝置“會根據註冊的信息得到相應的數據。從始至終kernel只會使用圖中這個黃黃的線程,無需再對額外的線程或者進程進行管理,提升了效率。

select組成結構

select的實現經歷了多個版本的修改,當前版本爲:1.11
select這個語句底層實現實際上主要由兩部分組成:case語句執行函數
源碼地址爲:/go/src/runtime/select.go

每個case語句,單獨抽象出以下結構體:

type scase struct {
    c           *hchan         // chan
    elem        unsafe.Pointer // 讀或者寫的緩衝區地址
    kind        uint16   //case語句的類型,是default、傳值寫數據(channel <-) 還是  取值讀數據(<- channel)
    pc          uintptr // race pc (for race detector / msan)
    releasetime int64
}

結構體可以用下圖表示:


其中比較關鍵的是:hchan,它是channel的指針。
在一個select中,所有的case語句會構成一個scase結構體的數組。

然後執行select語句實際上就是調用func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)函數。

func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)函數參數:

  • cas0 爲上文提到的case語句抽象出的結構體scase數組的第一個元素地址
  • order0爲一個兩倍cas0數組長度的buffer,保存scase隨機序列pollorder和scase中channel地址序列lockorder。
  • nncases表示scase數組的長度

selectgo返回所選scase的索引(該索引與其各自的select {recv,send,default}調用的序號位置相匹配)。此外,如果選擇的scase是接收操作(recv),則返回是否接收到值。

誰負責調用func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)函數呢?

/reflect/value.go中有個func rselect([]runtimeSelect) (chosen int, recvOK bool)函數,此函數的實現在/runtime/select.go文件中的func reflect_rselect(cases []runtimeSelect) (int, bool)函數中:

func reflect_rselect(cases []runtimeSelect) (int, bool) { 
    //如果cases語句爲空,則阻塞當前groutine
    if len(cases) == 0 {
        block()
    }
    //實例化case的結構體
    sel := make([]scase, len(cases))
    order := make([]uint16, 2*len(cases))
    for i := range cases {
        rc := &cases[i]
        switch rc.dir {
        case selectDefault:
            sel[i] = scase{kind: caseDefault}
        case selectSend:
            sel[i] = scase{kind: caseSend, c: rc.ch, elem: rc.val}
        case selectRecv:
            sel[i] = scase{kind: caseRecv, c: rc.ch, elem: rc.val}
        }
        if raceenabled || msanenabled {
            selectsetpc(&sel[i])
        }
    }
    return selectgo(&sel[0], &order[0], len(cases))
}

那誰調用的func rselect([]runtimeSelect) (chosen int, recvOK bool)呢?
/refect/value.go中,有一個func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool)的函數,其調用了rselect函數,並將最終Go中select語句的返回值的返回。

以上這三個函數的調用棧按順序如下:

  • func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool)
  • func rselect([]runtimeSelect) (chosen int, recvOK bool)
  • func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)

這仨函數中無論是返回值還是參數都大同小異,可以簡單粗暴的認爲:函數參數傳入的是case語句,返回值返回被選中的case語句。
那誰調用了func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool)呢?
可以簡單的認爲是系統了。
來個簡單的圖:

前兩個函數Selectrselect都是做了簡單的初始化參數,調用下一個函數的操作。select真正的核心功能,是在最後一個函數func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)中實現的。

selectgo函數做了什麼

打亂傳入的case結構體順序

鎖住其中的所有的channel

遍歷所有的channel,查看其是否可讀或者可寫

如果其中的channel可讀或者可寫,則解鎖所有channel,並返回對應的channel數據

假如沒有channel可讀或者可寫,但是有default語句,則同上:返回default語句對應的scase並解鎖所有的channel。

假如既沒有channel可讀或者可寫,也沒有default語句,則將當前運行的groutine阻塞,並加入到當前所有channel的等待隊列中去。

然後解鎖所有channel,等待被喚醒。

此時如果有個channel可讀或者可寫ready了,則喚醒,並再次加鎖所有channel,

遍歷所有channel找到那個對應的channel和G,喚醒G,並將沒有成功的G從所有channel的等待隊列中移除。

如果對應的scase值不爲空,則返回需要的值,並解鎖所有channel

如果對應的scase爲空,則循環此過程。

select和channel之間的關係

在想想select和channel做了什麼事兒,我覺得和多路複用是一回事兒

更多精彩內容,請關注我的微信公衆號 互聯網技術窩 或者加微信共同探討交流:

參考文獻:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章