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

在默認(rèn)的情況下,select 語句會(huì)在編譯階段經(jīng)過如下過程的處理:
- 將所有的
case轉(zhuǎn)換成包含Channel以及類型等信息的 scase 結(jié)構(gòu)體; - 調(diào)用運(yùn)行時(shí)函數(shù)
selectgo獲取被選擇的scase結(jié)構(gòu)體索引,如果當(dāng)前的scase是一個(gè)接收數(shù)據(jù)的操作,還會(huì)返回一個(gè)指示當(dāng)前case是否是接收的布爾值; - 通過
for循環(huán)生成一組if語句,在語句中判斷自己是不是被選中的case。
case數(shù)據(jù)結(jié)構(gòu)
select控制結(jié)構(gòu)中case使用了scase結(jié)構(gòu)體來表示,源碼包src/runtime/select.go:scase定義了表示case語句的數(shù)據(jù)結(jié)構(gòu):
type scase struct {
c *hchan
elem unsafe.Pointer
kind uint16
pc uintptr
releasetime int64
}scase.c:由于非default的case中都與channel的發(fā)送和接收數(shù)據(jù)有關(guān),所以在scase結(jié)構(gòu)體中也包含一個(gè)c字段用于存儲(chǔ)case中使用的channel,為當(dāng)前case語句所操作的channel指針,這也說明了一個(gè)case語句只能操作一個(gè)channel。
scase.kind:表示該case的類型,分為讀channel、寫channel和default,三種類型分別由常量定義:
const (
caseNil = iota
caseRecv //case語句中嘗試讀取scase.c中的數(shù)據(jù);
caseSend //case語句中嘗試向scase.c中寫入數(shù)據(jù);
caseDefault //default語句
)scase.elem:用于接收或者發(fā)送數(shù)據(jù)的變量地址,根據(jù)scase.kind不同,有不同的用途:
- scase.kind == caseRecv :
scase.elem表示讀出channel的數(shù)據(jù)存放地址; - scase.kind == caseSend :
scase.elem表示將要寫入channel的數(shù)據(jù)存放地址;
執(zhí)行select
在運(yùn)行期間會(huì)調(diào)用selectgo()函數(shù),這個(gè)函數(shù)主要作用是從select控制結(jié)構(gòu)中的多個(gè)case中選擇一個(gè)需要執(zhí)行的case,隨后的多個(gè) if 條件語句就會(huì)根據(jù) selectgo() 的返回值執(zhí)行相應(yīng)的語句。
運(yùn)行時(shí)源碼包src/runtime/select.go:selectgo()定義了select選擇case的函數(shù):
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 函數(shù)首先會(huì)進(jìn)行執(zhí)行一些初始化操作,也就是決定處理 case 的兩個(gè)順序,其中一個(gè)是 pollOrder 另一個(gè)是 lockOrder。
函數(shù)參數(shù):
- cas0:為scase數(shù)組的首地址,
selectgo()就是從這些scase中找出一個(gè)返回。 - order0:為一個(gè)兩倍cas0數(shù)組長度的buffer,保存scase隨機(jī)序列
pollorder和scase中channel地址序列lockorder; - pollorder:每次
selectgo執(zhí)行都會(huì)把scase序列打亂,以達(dá)到隨機(jī)檢測(cè)case的目的。 - lockorder:所有
case語句中channel序列,以達(dá)到去重防止對(duì)channel加鎖時(shí)重復(fù)加鎖的目的。 - ncases:表示
scase數(shù)組的長度
函數(shù)返回值:
- int: 選中
case的編號(hào),這個(gè)case編號(hào)跟代碼一致 - bool: 是否成功從
channle中讀取了數(shù)據(jù),如果選中的case是從channel中讀數(shù)據(jù),則該返回值表示是否讀取成功。
循環(huán)
當(dāng) select 語句確定了輪詢和鎖定的順序并鎖定了所有的 Channel 之后就會(huì)開始進(jìn)入 select的主循環(huán),查找或者等待 Channel 準(zhǔn)備就緒,循環(huán)中會(huì)遍歷所有的 case 并找到需要被喚起的sudog 結(jié)構(gòu)體。
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)的代碼中,我們會(huì)分四種不同的情況處理 select 中的多個(gè) case:
caseNil — 當(dāng)前 case 不包含任何的 Channel,就直接會(huì)被跳過;
caseRecv — 當(dāng)前 case 會(huì)從 Channel 中接收數(shù)據(jù);
- 如果當(dāng)前
Channel的sendq上有等待的Goroutine就會(huì)直接跳到recv標(biāo)簽所在的代碼段,從Goroutine中獲取最新發(fā)送的數(shù)據(jù); - 如果當(dāng)前
Channel的緩沖區(qū)不為空就會(huì)跳到bufrecv標(biāo)簽處從緩沖區(qū)中獲取數(shù)據(jù); - 如果當(dāng)前
Channel已經(jīng)被關(guān)閉就會(huì)跳到rclose做一些清除的收尾工作;
caseSend — 當(dāng)前 case 會(huì)向 Channel 發(fā)送數(shù)據(jù);
- 如果當(dāng)前
Channel已經(jīng)被關(guān)閉就會(huì)直接跳到rclose代碼段; - 如果當(dāng)前
Channel的recvq上有等待的Goroutine就會(huì)跳到send代碼段向Channel直接發(fā)送數(shù)據(jù);
caseDefault — 表示默認(rèn)情況,如果循環(huán)執(zhí)行到了這種情況就表示前面的所有case 都沒有被執(zhí)行,所以這里會(huì)直接解鎖所有的 Channel 并退出 selectgo 函數(shù),這時(shí)也就意味著當(dāng)前 select 結(jié)構(gòu)中的其他收發(fā)語句都是非阻塞的。
總結(jié)
通過以上內(nèi)容我們簡(jiǎn)單的了解了select結(jié)構(gòu)的執(zhí)行過程與實(shí)現(xiàn)原理,首先在編譯期間,Go 語言會(huì)對(duì) select 語句進(jìn)行優(yōu)化, 對(duì)于空的select語句會(huì)直接轉(zhuǎn)換成block函數(shù)的調(diào)用,直接掛起當(dāng)前Goroutine,如果select語句中只包含一個(gè)case,就會(huì)被轉(zhuǎn)換成if ch == nil {block}; n; 表達(dá)式。然后執(zhí)行case結(jié)構(gòu)體中內(nèi)容。
在運(yùn)行時(shí)會(huì)執(zhí)行selectgo函數(shù),隨機(jī)生成一個(gè)遍歷的輪詢順序pollOrder并根據(jù)Channel地址生成一個(gè)用于遍歷的鎖定順序lockOrder;然后根據(jù)pollOrder遍歷所有的case查看是否有可以處理的Channel消息,如果有消息就直接獲取case對(duì)應(yīng)的索引并返回。如果沒有消息就會(huì)創(chuàng)建sudog結(jié)構(gòu)體,將當(dāng)前 Goroutine 加入到所有相關(guān) Channel 的sendq 和 recvq 隊(duì)列中并調(diào)用 gopark 觸發(fā)調(diào)度器的調(diào)度;
注意: 并不是所有的select控制結(jié)構(gòu)都會(huì)走到selectgo,很多情況都會(huì)被直接優(yōu)化調(diào)。
以上就是深入淺出Golang中select的實(shí)現(xiàn)原理的詳細(xì)內(nèi)容,更多關(guān)于Golang select原理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
GoFrame?gmap遍歷hashmap?listmap?treemap使用技巧
這篇文章主要為大家介紹了GoFrame?gmap遍歷hashmap?listmap?treemap使用技巧的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
Go語言集成開發(fā)環(huán)境IDE詳細(xì)安裝教程
VSCode是免費(fèi)開源的現(xiàn)代化輕量級(jí)代碼編輯器,支持幾乎所有主流的開發(fā)語言,內(nèi)置命令行工具和 Git 版本控制系統(tǒng),支持插件擴(kuò)展,這篇文章主要介紹了Go語言集成開發(fā)環(huán)境IDE詳細(xì)安裝教程,需要的朋友可以參考下2021-11-11
Golang?Gin框架獲取請(qǐng)求參數(shù)的幾種常見方式
在我們平常添加路由處理函數(shù)之后,就可以在路由處理函數(shù)中編寫業(yè)務(wù)處理代碼了,但在此之前我們往往需要獲取請(qǐng)求參數(shù),本文就詳細(xì)的講解下gin獲取請(qǐng)求參數(shù)常見的幾種方式,需要的朋友可以參考下2024-02-02

