Go 的select語句是一種僅能用於channl發送和接收消息的專用語句,此語句運行期間是阻塞的;當select中沒有case語句的時候,會阻塞當前的groutine。因此,有人也會說select是用來阻塞監聽goroutine的。 還有人說:select是Golang在語言層面提供的I/O多路複用的機制,其專門用來檢測多個channel是否準備完畢:可讀或可寫。golang
以上說法都正確。數組
咱們來回顧一下是什麼是I/O多路複用
。微信
每來一個進程,都會創建鏈接,而後阻塞,直到接收到數據返回響應。 普通這種方式的缺點其實很明顯:系統須要建立和維護額外的線程或進程。由於大多數時候,大部分阻塞的線程或進程是處於等待狀態,只有少部分會接收並處理響應,而其他的都在等待。系統爲此還須要多作不少額外的線程或者進程的管理工做。多線程
爲了解決圖中這些多餘的線程或者進程,因而有了"I/O多路複用"函數
每一個線程或者進程都先到圖中」裝置「中註冊,而後阻塞,而後只有一個線程在」運輸「,當註冊的線程或者進程準備好數據後,」裝置「會根據註冊的信息獲得相應的數據。從始至終kernel只會使用圖中這個黃黃的線程,無需再對額外的線程或者進程進行管理,提高了效率。ui
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)
函數參數:
scase
數組的第一個元素地址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)
呢? 能夠簡單的認爲是系統了。 來個簡單的圖:
前兩個函數Select
和rselect
都是作了簡單的初始化參數,調用下一個函數的操做。select真正的核心功能,是在最後一個函數func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)
中實現的。
打亂傳入的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作了什麼事兒,我以爲和多路複用是一回事兒
互聯網技術窩
或者加微信共同探討交流:參考文獻: