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