圖解Go select語句原理

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

以上說法都正確。數組

I/O多路複用

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

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

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

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

I/O多路複用

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

select組成結構

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

每一個case語句,單獨抽象出如下結構體:.net

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)函數。3d

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作了什麼事兒,我以爲和多路複用是一回事兒

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

參考文獻:

相關文章
相關標籤/搜索