深入淺出Golang中select的實現原理
概述
在go語言中,select語句就是用來監(jiān)聽和channel有關的IO操作,當IO操作發(fā)生時,觸發(fā)相應的case操作,有了select語句,可以實現main主線程與goroutine線程之間的互動。需要的朋友可以參考以下內容,希望對大家有幫助。
select實現原理
Golang實現select時,定義了一個數據結構表示每個case語句(包含default,default實際上是一種特殊的case),select執(zhí)行過程可以看成一個函數,函數輸入case數組,輸出選中的case,然后程序流程轉到選中的case塊。
執(zhí)行流程

在默認的情況下,select 語句會在編譯階段經過如下過程的處理:
- 將所有的
case轉換成包含Channel以及類型等信息的 scase 結構體; - 調用運行時函數
selectgo獲取被選擇的scase結構體索引,如果當前的scase是一個接收數據的操作,還會返回一個指示當前case是否是接收的布爾值; - 通過
for循環(huán)生成一組if語句,在語句中判斷自己是不是被選中的case。
case數據結構
select控制結構中case使用了scase結構體來表示,源碼包src/runtime/select.go:scase定義了表示case語句的數據結構:
type scase struct {
c *hchan
elem unsafe.Pointer
kind uint16
pc uintptr
releasetime int64
}scase.c:由于非default的case中都與channel的發(fā)送和接收數據有關,所以在scase結構體中也包含一個c字段用于存儲case中使用的channel,為當前case語句所操作的channel指針,這也說明了一個case語句只能操作一個channel。
scase.kind:表示該case的類型,分為讀channel、寫channel和default,三種類型分別由常量定義:
const (
caseNil = iota
caseRecv //case語句中嘗試讀取scase.c中的數據;
caseSend //case語句中嘗試向scase.c中寫入數據;
caseDefault //default語句
)scase.elem:用于接收或者發(fā)送數據的變量地址,根據scase.kind不同,有不同的用途:
- scase.kind == caseRecv :
scase.elem表示讀出channel的數據存放地址; - scase.kind == caseSend :
scase.elem表示將要寫入channel的數據存放地址;
執(zhí)行select
在運行期間會調用selectgo()函數,這個函數主要作用是從select控制結構中的多個case中選擇一個需要執(zhí)行的case,隨后的多個 if 條件語句就會根據 selectgo() 的返回值執(zhí)行相應的語句。
運行時源碼包src/runtime/select.go:selectgo()定義了select選擇case的函數:
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0))
order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0))
scases := cas1[:ncases:ncases]
pollorder := order1[:ncases:ncases]
lockorder := order1[ncases:][:ncases:ncases]
for i := range scases {
cas := &scases[i]
if cas.c == nil && cas.kind != caseDefault {
*cas = scase{}
}
}
for i := 1; i < ncases; i++ {
j := fastrandn(uint32(i + 1))
pollorder[i] = pollorder[j]
pollorder[j] = uint16(i)
}
// sort the cases by Hchan address to get the locking order.
// ...
sellock(scases, lockorder)
// ...
}selectgo 函數首先會進行執(zhí)行一些初始化操作,也就是決定處理 case 的兩個順序,其中一個是 pollOrder 另一個是 lockOrder。
函數參數:
- cas0:為scase數組的首地址,
selectgo()就是從這些scase中找出一個返回。 - order0:為一個兩倍cas0數組長度的buffer,保存scase隨機序列
pollorder和scase中channel地址序列lockorder; - pollorder:每次
selectgo執(zhí)行都會把scase序列打亂,以達到隨機檢測case的目的。 - lockorder:所有
case語句中channel序列,以達到去重防止對channel加鎖時重復加鎖的目的。 - ncases:表示
scase數組的長度
函數返回值:
- int: 選中
case的編號,這個case編號跟代碼一致 - bool: 是否成功從
channle中讀取了數據,如果選中的case是從channel中讀數據,則該返回值表示是否讀取成功。
循環(huán)
當 select 語句確定了輪詢和鎖定的順序并鎖定了所有的 Channel 之后就會開始進入 select的主循環(huán),查找或者等待 Channel 準備就緒,循環(huán)中會遍歷所有的 case 并找到需要被喚起的sudog 結構體。
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
// ...
gp = getg()
nextp = &gp.waiting
for _, casei := range lockorder {
casi = int(casei)
cas = &scases[casi]
if cas.kind == caseNil {
continue
}
c = cas.c
sg := acquireSudog()
sg.g = gp
sg.isSelect = true
sg.elem = cas.elem
sg.c = c
*nextp = sg
nextp = &sg.waitlink
switch cas.kind {
case caseRecv:
c.recvq.enqueue(sg)
case caseSend:
c.sendq.enqueue(sg)
}
}
gp.param = nil
gopark(selparkcommit, nil, waitReasonSelect, traceEvGoBlockSelect, 1)
// ...
}在這段循環(huán)的代碼中,我們會分四種不同的情況處理 select 中的多個 case:
caseNil — 當前 case 不包含任何的 Channel,就直接會被跳過;
caseRecv — 當前 case 會從 Channel 中接收數據;
- 如果當前
Channel的sendq上有等待的Goroutine就會直接跳到recv標簽所在的代碼段,從Goroutine中獲取最新發(fā)送的數據; - 如果當前
Channel的緩沖區(qū)不為空就會跳到bufrecv標簽處從緩沖區(qū)中獲取數據; - 如果當前
Channel已經被關閉就會跳到rclose做一些清除的收尾工作;
caseSend — 當前 case 會向 Channel 發(fā)送數據;
- 如果當前
Channel已經被關閉就會直接跳到rclose代碼段; - 如果當前
Channel的recvq上有等待的Goroutine就會跳到send代碼段向Channel直接發(fā)送數據;
caseDefault — 表示默認情況,如果循環(huán)執(zhí)行到了這種情況就表示前面的所有case 都沒有被執(zhí)行,所以這里會直接解鎖所有的 Channel 并退出 selectgo 函數,這時也就意味著當前 select 結構中的其他收發(fā)語句都是非阻塞的。
總結
通過以上內容我們簡單的了解了select結構的執(zhí)行過程與實現原理,首先在編譯期間,Go 語言會對 select 語句進行優(yōu)化, 對于空的select語句會直接轉換成block函數的調用,直接掛起當前Goroutine,如果select語句中只包含一個case,就會被轉換成if ch == nil {block}; n; 表達式。然后執(zhí)行case結構體中內容。
在運行時會執(zhí)行selectgo函數,隨機生成一個遍歷的輪詢順序pollOrder并根據Channel地址生成一個用于遍歷的鎖定順序lockOrder;然后根據pollOrder遍歷所有的case查看是否有可以處理的Channel消息,如果有消息就直接獲取case對應的索引并返回。如果沒有消息就會創(chuàng)建sudog結構體,將當前 Goroutine 加入到所有相關 Channel 的sendq 和 recvq 隊列中并調用 gopark 觸發(fā)調度器的調度;
注意: 并不是所有的select控制結構都會走到selectgo,很多情況都會被直接優(yōu)化調。
以上就是深入淺出Golang中select的實現原理的詳細內容,更多關于Golang select原理的資料請關注腳本之家其它相關文章!
相關文章
GoFrame?gmap遍歷hashmap?listmap?treemap使用技巧
這篇文章主要為大家介紹了GoFrame?gmap遍歷hashmap?listmap?treemap使用技巧的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-06-06

