Go 語(yǔ)言 select 的實(shí)現(xiàn)原理解析
介紹
select是Go在語(yǔ)言層面提供的I/O多路復(fù)用的機(jī)制,其專門(mén)用來(lái)讓Goroutine同時(shí)等待多個(gè)channel是否準(zhǔn)備完畢:可讀或可寫(xiě)。在Channel狀態(tài)改變之前,select會(huì)一直阻塞當(dāng)前線程或者goroutine。
特性:
case 必須是一個(gè)通信操作,主要是指對(duì)通道(Channel)進(jìn)行發(fā)送或者接收數(shù)據(jù)的操作。
select 語(yǔ)句中除 default 外,各 case 執(zhí)行順序是隨機(jī)的。
select 語(yǔ)句中如果沒(méi)有 default 語(yǔ)句,則會(huì)阻塞等待任意一個(gè) case滿足執(zhí)行條件。
select 語(yǔ)句中除 default 外,每個(gè) case 只能操作一個(gè) channel,要么讀要么寫(xiě)。
當(dāng) select 中的多個(gè) case 同時(shí)被觸發(fā)時(shí),會(huì)隨機(jī)執(zhí)行其中的一個(gè)。
普通多線程

多路復(fù)用

數(shù)據(jù)結(jié)構(gòu)
select在Go語(yǔ)言的源代碼中不存在對(duì)應(yīng)的結(jié)構(gòu)體,使用runtime.scase 結(jié)構(gòu)體表示select控制結(jié)構(gòu)里的case。
type scase struct {
c *hchan //case操作的通道
kind uint16
//表示該case的類型,分為讀channel、寫(xiě)channel和default。
//讀channel、寫(xiě)channel和default三種類型分別由常量定義
//caseRecv:case語(yǔ)句中嘗試讀取scase.c中的數(shù)據(jù)。
//caseSend:case語(yǔ)句中嘗試向scase.c中寫(xiě)入數(shù)據(jù)。
//caseDefault:default語(yǔ)句。
elem unsafe.Pointer
//scase.kind == caseRecv : scase.elem表示讀出channel的數(shù)據(jù)存放地址;
//scase.kind == caseSend : scase.elem表示將要寫(xiě)入channel的數(shù)據(jù)存放地址;
}在select語(yǔ)句運(yùn)行時(shí),scase結(jié)構(gòu)體的實(shí)例會(huì)被用來(lái)表示每個(gè)case。運(yùn)行時(shí)根據(jù)c 字段找到對(duì)應(yīng)的通道,根據(jù)elem字段來(lái)處理數(shù)據(jù)的發(fā)送或接收操作。
執(zhí)行流程

實(shí)現(xiàn)過(guò)程和結(jié)果(穿插編譯器的重寫(xiě)和優(yōu)化)
單分支的select
只有一個(gè) case 且不是 default,這種情況編譯器會(huì)直接將其翻譯成對(duì)管道的收發(fā)操作,并且還是阻塞式的,一直阻塞到操作可以完成。
對(duì)于接收操作(如 case val := <-ch),它會(huì)被轉(zhuǎn)換為 val := <-ch,直接嘗試從通道 ch 接收數(shù)據(jù)。
對(duì)于發(fā)送操作(如 case ch <- value),它會(huì)被轉(zhuǎn)換為 ch <- value,直接嘗試向通道 ch 發(fā)送數(shù)據(jù)。
只包含default分支會(huì)直接執(zhí)行default操作。
多路select
在編譯器中會(huì)被轉(zhuǎn)換為runtime.selectgo函數(shù)調(diào)用。
func selectgo(cas0 *scase, order0 *uint16, , ncases int) (int, bool) {
pollorder := order1[:ncases:ncases]
lockorder := order1[ncases:][:ncases:ncases]
for i := 1; i < ncases; i++ {
j := fastrandn(uint32(i + 1))
pollorder[i] = pollorder[j]
pollorder[j] = uint16(i)
}
// 代碼可能繼續(xù)執(zhí)行后續(xù)操作
}- cas0,scase數(shù)組的頭部指針,前半部分存放的是寫(xiě)管道 case,后半部分存放的讀管道 case,以nsends來(lái)區(qū)分
- order0,它的長(zhǎng)度是scase數(shù)組的兩倍,前半部分分配給pollorder數(shù)組(決定管道執(zhí)行順序),后半部分分配給lockorder數(shù)組(決定管道鎖定順序)
- pollorder:每次selectgo執(zhí)行都會(huì)把scase序列打亂,以達(dá)到隨機(jī)檢測(cè)case的目的。
- lockorder:所有case語(yǔ)句中channel序列,以達(dá)到去重防止對(duì)channel加鎖時(shí)重復(fù)加鎖的目的。
- ncases表示scase數(shù)組的長(zhǎng)度
直接阻塞
1. select結(jié)構(gòu)不包含任何case
在Go編譯器內(nèi)部的代碼如下
func walkselectcases(cases *Nodes) []*Node {
n := cases.Len()
if n == 0 {
return []*Node{mkcall("block", nil, nil)}
}
...
}
func block() {
gopark(nil, nil, waitReasonSelectNoCases, traceEvGoStop, 1)
}walkselectcases的參數(shù)是一個(gè)select語(yǔ)句中的case元素的集合。當(dāng)集合的長(zhǎng)度為0時(shí),表示當(dāng)前select中無(wú)case會(huì)調(diào)用block函數(shù),block函數(shù)會(huì)調(diào)用gopark讓出goroutine對(duì)處理器的使用權(quán)并傳入等待原因,暫停goroutine避免CPU空轉(zhuǎn)。
2.當(dāng)case中的channel是空指針
例:包含一個(gè)case且case中的channel是空指針,編譯器會(huì)將select改寫(xiě)為if條件語(yǔ)句
//部分代碼
if ch == nil {
block()調(diào)用block,將goroutine陷入永久休眠
}非阻塞操作
當(dāng)select中包含default分支時(shí),就會(huì)被編譯器認(rèn)為是一次非阻塞的收發(fā)操作。
示例:一個(gè)case分支一個(gè)default分支,對(duì)通道的讀寫(xiě)操作
示例:一個(gè)case分支一個(gè)default分支,對(duì)通道的讀寫(xiě)操作
寫(xiě)操作:編譯器會(huì)使用條件語(yǔ)句和 runtime.selectnbsend 函數(shù)改寫(xiě)代碼
//改寫(xiě)
if selectnbsend(ch, i) {
...
} else {
...
}
//false參數(shù)決定了這一次的發(fā)送是非阻塞的,所以如果存在緩沖區(qū)空間不足時(shí),當(dāng)前 Goroutine 都不會(huì)阻塞而是會(huì)直接返回。
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
return chansend(c, elem, false, getcallerpc())
}讀操作:
// 改寫(xiě)前
select {
case v <- ch: // case v, ok <- ch:
......
default:
......
}
// 改寫(xiě)后
if selectnbrecv(&v, ch) { // if selectnbrecv2(&v, &ok, ch) {
...
} else {
...
}
//看讀操作是否需要,第一個(gè)會(huì)忽略返回的布爾值,第二個(gè)會(huì)將布爾值傳給調(diào)用方,block參數(shù)決定本次操作不阻塞
func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected bool) {
selected, _ = chanrecv(c, elem, false)
return
}
func selectnbrecv2(elem unsafe.Pointer, received *bool, c *hchan) (selected bool) {
selected, *received = chanrecv(c, elem, false)
return
}性能優(yōu)化建議
- case數(shù)量控制建議不超過(guò)5-10個(gè)
- 適當(dāng)使用帶緩沖區(qū)的channel避免頻繁的阻塞和喚醒
- 合理使用default避免無(wú)謂的阻塞
到此這篇關(guān)于Go 語(yǔ)言 select 的實(shí)現(xiàn)原理的文章就介紹到這了,更多相關(guān)Go 語(yǔ)言 select內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
GoFrame框架Scan類型轉(zhuǎn)換實(shí)例
這篇文章主要為大家介紹了GoFrame框架Scan類型轉(zhuǎn)換的實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
Go的固定時(shí)長(zhǎng)定時(shí)器和周期性時(shí)長(zhǎng)定時(shí)器
本文主要介紹了Go的固定時(shí)長(zhǎng)定時(shí)器和周期性時(shí)長(zhǎng)定時(shí)器,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08
Go語(yǔ)言并發(fā)編程基礎(chǔ)上下文概念詳解
這篇文章主要為大家介紹了Go語(yǔ)言并發(fā)編程基礎(chǔ)上下文示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
基于Go語(yǔ)言開(kāi)發(fā)一個(gè)編解碼工具
這篇文章主要為大家詳細(xì)介紹了如何基于Go語(yǔ)言開(kāi)發(fā)一個(gè)編解碼工具,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,感興趣的小伙伴可以跟隨小編一起了解一下2025-03-03
golang 數(shù)組去重,利用map的實(shí)現(xiàn)
這篇文章主要介紹了golang 數(shù)組去重,利用map的實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04
基于Golang實(shí)現(xiàn)延遲隊(duì)列(DelayQueue)
延遲隊(duì)列是一種特殊的隊(duì)列,元素入隊(duì)時(shí)需要指定到期時(shí)間(或延遲時(shí)間),從隊(duì)頭出隊(duì)的元素必須是已經(jīng)到期的。本文將用Golang實(shí)現(xiàn)延遲隊(duì)列,感興趣的可以了解下2022-09-09
golang通過(guò)遞歸遍歷生成樹(shù)狀結(jié)構(gòu)的操作
這篇文章主要介紹了golang通過(guò)遞歸遍歷生成樹(shù)狀結(jié)構(gòu)的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-04-04

