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)
呢?
可以簡單的認為是系統了。
來個簡單的圖:
前兩個函式Select
和rselect
都是做了簡單的初始化引數,呼叫下一個函式的操作。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做了什麼事兒,我覺得和多路複用是一回事兒
更多精彩內容,請關注我的微信公眾號 網際網路技術窩
或者加微信共同探討交流:
參考文獻: