深入淺出Golang中select的實(shí)現(xiàn)原理
概述
在go語(yǔ)言中,select
語(yǔ)句就是用來(lái)監(jiān)聽(tīng)和channel
有關(guān)的IO
操作,當(dāng)IO
操作發(fā)生時(shí),觸發(fā)相應(yīng)的case
操作,有了select
語(yǔ)句,可以實(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
語(yǔ)句(包含default
,default
實(shí)際上是一種特殊的case
),select
執(zhí)行過(guò)程可以看成一個(gè)函數(shù),函數(shù)輸入case
數(shù)組,輸出選中的case
,然后程序流程轉(zhuǎn)到選中的case
塊。
執(zhí)行流程
在默認(rèn)的情況下,select 語(yǔ)句會(huì)在編譯階段經(jīng)過(guò)如下過(guò)程的處理:
- 將所有的
case
轉(zhuǎn)換成包含Channel
以及類(lèi)型等信息的 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
是否是接收的布爾值; - 通過(guò)
for
循環(huán)生成一組if
語(yǔ)句,在語(yǔ)句中判斷自己是不是被選中的case
。
case數(shù)據(jù)結(jié)構(gòu)
select
控制結(jié)構(gòu)中case
使用了scase
結(jié)構(gòu)體來(lái)表示,源碼包src/runtime/select.go:scase
定義了表示case
語(yǔ)句的數(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
語(yǔ)句所操作的channel
指針,這也說(shuō)明了一個(gè)case
語(yǔ)句只能操作一個(gè)channel
。
scase.kind:表示該case的類(lèi)型,分為讀channel
、寫(xiě)channel
和default
,三種類(lèi)型分別由常量定義:
const ( caseNil = iota caseRecv //case語(yǔ)句中嘗試讀取scase.c中的數(shù)據(jù); caseSend //case語(yǔ)句中嘗試向scase.c中寫(xiě)入數(shù)據(jù); caseDefault //default語(yǔ)句 )
scase.elem:用于接收或者發(fā)送數(shù)據(jù)的變量地址,根據(jù)scase.kind
不同,有不同的用途:
- scase.kind == caseRecv :
scase.elem
表示讀出channel
的數(shù)據(jù)存放地址; - scase.kind == caseSend :
scase.elem
表示將要寫(xiě)入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
條件語(yǔ)句就會(huì)根據(jù) selectgo()
的返回值執(zhí)行相應(yīng)的語(yǔ)句。
運(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ù)組長(zhǎng)度的buffer,保存scase隨機(jī)序列
pollorder
和scase
中channel
地址序列lockorder
; - pollorder:每次
selectgo
執(zhí)行都會(huì)把scase
序列打亂,以達(dá)到隨機(jī)檢測(cè)case
的目的。 - lockorder:所有
case
語(yǔ)句中channel
序列,以達(dá)到去重防止對(duì)channel
加鎖時(shí)重復(fù)加鎖的目的。 - ncases:表示
scase
數(shù)組的長(zhǎng)度
函數(shù)返回值:
- int: 選中
case
的編號(hào),這個(gè)case
編號(hào)跟代碼一致 - bool: 是否成功從
channle
中讀取了數(shù)據(jù),如果選中的case
是從channel
中讀數(shù)據(jù),則該返回值表示是否讀取成功。
循環(huán)
當(dāng) select 語(yǔ)句確定了輪詢(xún)和鎖定的順序并鎖定了所有的 Channel
之后就會(huì)開(kāi)始進(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ì)被跳過(guò);
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
都沒(méi)有被執(zhí)行,所以這里會(huì)直接解鎖所有的 Channel
并退出 selectgo
函數(shù),這時(shí)也就意味著當(dāng)前 select
結(jié)構(gòu)中的其他收發(fā)語(yǔ)句都是非阻塞的。
總結(jié)
通過(guò)以上內(nèi)容我們簡(jiǎn)單的了解了select
結(jié)構(gòu)的執(zhí)行過(guò)程與實(shí)現(xiàn)原理,首先在編譯期間,Go
語(yǔ)言會(huì)對(duì) select
語(yǔ)句進(jìn)行優(yōu)化, 對(duì)于空的select
語(yǔ)句會(huì)直接轉(zhuǎn)換成block
函數(shù)的調(diào)用,直接掛起當(dāng)前Goroutine
,如果select
語(yǔ)句中只包含一個(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è)遍歷的輪詢(xún)順序pollOrder
并根據(jù)Channel
地址生成一個(gè)用于遍歷的鎖定順序lockOrder
;然后根據(jù)pollOrder
遍歷所有的case
查看是否有可以處理的Channel
消息,如果有消息就直接獲取case對(duì)應(yīng)的索引并返回。如果沒(méi)有消息就會(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-06Go語(yǔ)言集成開(kāi)發(fā)環(huán)境IDE詳細(xì)安裝教程
VSCode是免費(fèi)開(kāi)源的現(xiàn)代化輕量級(jí)代碼編輯器,支持幾乎所有主流的開(kāi)發(fā)語(yǔ)言,內(nèi)置命令行工具和 Git 版本控制系統(tǒng),支持插件擴(kuò)展,這篇文章主要介紹了Go語(yǔ)言集成開(kāi)發(fā)環(huán)境IDE詳細(xì)安裝教程,需要的朋友可以參考下2021-11-11Golang?Gin框架獲取請(qǐng)求參數(shù)的幾種常見(jiàn)方式
在我們平常添加路由處理函數(shù)之后,就可以在路由處理函數(shù)中編寫(xiě)業(yè)務(wù)處理代碼了,但在此之前我們往往需要獲取請(qǐng)求參數(shù),本文就詳細(xì)的講解下gin獲取請(qǐng)求參數(shù)常見(jiàn)的幾種方式,需要的朋友可以參考下2024-02-02Go編程中常見(jiàn)錯(cuò)誤和不良實(shí)踐解析
這篇文章主要為大家介紹了Go編程中常見(jiàn)錯(cuò)誤和不良實(shí)踐解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01