一文帶你了解Golang中select的實(shí)現(xiàn)原理
概述
select是go提供的一種跟并發(fā)相關(guān)的語法,非常有用。本文將介紹 Go 語言中的 select
的實(shí)現(xiàn)原理,包括 select
的結(jié)構(gòu)和常見問題、編譯期間的多種優(yōu)化以及運(yùn)行時(shí)的執(zhí)行過程。
select
是一種與 switch
非常相似的控制結(jié)構(gòu),與 switch
不同的是,select
中雖然也有多個(gè) case
,但是這些 case
中的表達(dá)式都必須與 Channel 的操作有關(guān),也就是 Channel 的讀寫操作,下面的函數(shù)就展示了一個(gè)包含從 Channel 中讀取數(shù)據(jù)和向 Channel 發(fā)送數(shù)據(jù)的 select
結(jié)構(gòu):
func fibonacci(c, quit chan int) { x, y := 0, 1 for { select { case c <- x: x, y = y, x+y case <-quit: fmt.Println("quit") return } } }
這個(gè) select
控制結(jié)構(gòu)就會(huì)等待 c <- x
或者 <-quit
兩個(gè)表達(dá)式中任意一個(gè)的返回,無論哪一個(gè)返回都會(huì)立刻執(zhí)行 case
中的代碼,不過如果了 select
中的兩個(gè) case
同時(shí)被觸發(fā),就會(huì)隨機(jī)選擇一個(gè) case
執(zhí)行。
結(jié)構(gòu)
select
在 Go 語言的源代碼中其實(shí)不存在任何的結(jié)構(gòu)體表示,但是 select
控制結(jié)構(gòu)中 case
卻使用了 scase
結(jié)構(gòu)體來表示:
type scase struct { c *hchan elem unsafe.Pointer kind uint16 pc uintptr releasetime int64 }
由于非 default
的 case
中都與 Channel 的發(fā)送和接收數(shù)據(jù)有關(guān),所以在 scase
結(jié)構(gòu)體中也包含一個(gè) c
字段用于存儲(chǔ) case
中使用的 Channel,elem
是用于接收或者發(fā)送數(shù)據(jù)的變量地址、kind
表示當(dāng)前 case
的種類,總共包含以下四種:
const ( caseNil = iota caseRecv caseSend caseDefault )
這四種常量分別表示不同類型的 case
,相信它們的命名已經(jīng)能夠充分幫助我們理解它們的作用了,所以在這里也不再展開介紹了。
現(xiàn)象
當(dāng)我們?cè)?Go 語言中使用 select
控制結(jié)構(gòu)時(shí),其實(shí)會(huì)遇到兩個(gè)非常有趣的問題,一個(gè)是 select
能在 Channel 上進(jìn)行非阻塞的收發(fā)操作,另一個(gè)是 select
在遇到多個(gè) Channel 同時(shí)響應(yīng)時(shí)能夠隨機(jī)挑選 case
執(zhí)行。
非阻塞的收發(fā)
如果一個(gè) select
控制結(jié)構(gòu)中包含一個(gè) default
表達(dá)式,那么這個(gè) select
并不會(huì)等待其它的 Channel 準(zhǔn)備就緒,而是會(huì)非阻塞地讀取或者寫入數(shù)據(jù):
func main() { ch := make(chan int) select { case i := <-ch: println(i) default: println("default") } } $ go run main.go default
當(dāng)我們運(yùn)行上面的代碼時(shí)其實(shí)也并不會(huì)阻塞當(dāng)前的 Goroutine,而是會(huì)直接執(zhí)行 default
條件中的內(nèi)容并返回。
隨機(jī)執(zhí)行
另一個(gè)使用 select
遇到的情況其實(shí)就是同時(shí)有多個(gè) case
就緒后,select
如何進(jìn)行選擇的問題,我們通過下面的代碼可以簡(jiǎn)單了解一下:
func main() { ch := make(chan int) go func() { for range time.Tick(1 * time.Second) { ch <- 0 } }() for { select { case <-ch: println("case1") case <-ch: println("case2") } } } $ go run main.go case1 case2 case1 case2 case2 case1 ...
從上述代碼輸出的結(jié)果中我們可以看到,select
在遇到兩個(gè) <-ch
同時(shí)響應(yīng)時(shí)其實(shí)會(huì)隨機(jī)選擇一個(gè) case
執(zhí)行其中的表達(dá)式,我們會(huì)在這一節(jié)中介紹這一現(xiàn)象的實(shí)現(xiàn)原理。
編譯
select
語句在編譯期間會(huì)被轉(zhuǎn)換成 OSELECT
節(jié)點(diǎn),每一個(gè) OSELECT
節(jié)點(diǎn)都會(huì)持有一系列的 OCASE
節(jié)點(diǎn),如果 OCASE
節(jié)點(diǎn)的都是空的,就意味著這是一個(gè) default
節(jié)點(diǎn):
上圖展示的其實(shí)就是 select 在編譯期間的結(jié)構(gòu),每一個(gè) OCASE 既包含了執(zhí)行條件也包含了滿足條件后執(zhí)行的代碼,我們?cè)谶@一節(jié)中就會(huì)介紹 select 語句在編譯期間進(jìn)行的優(yōu)化和轉(zhuǎn)換。
編譯器在中間代碼生成期間會(huì)根據(jù) select 中 case 的不同對(duì)控制語句進(jìn)行優(yōu)化,這一過程其實(shí)都發(fā)生在 walkselectcases 函數(shù)中,我們?cè)谶@里會(huì)分四種情況分別介紹優(yōu)化的過程和結(jié)果:
select 中不存在任何的 case;
select 中只存在一個(gè) case;
select 中存在兩個(gè) case,其中一個(gè) case 是 default 語句;
通用的 select 條件;
我們會(huì)按照這四種不同的情況拆分 walkselectcases 函數(shù)并分別介紹不同場(chǎng)景下優(yōu)化的結(jié)果。
直接阻塞
首先介紹的其實(shí)就是最簡(jiǎn)單的情況,也就是當(dāng) select
結(jié)構(gòu)中不包含任何的 case
時(shí),編譯器是如何進(jìn)行處理的:
func walkselectcases(cases *Nodes) []*Node { n := cases.Len() if n == 0 { return []*Node{mkcall("block", nil, nil)} } // ... }
這段代碼非常簡(jiǎn)單并且容易理解,它直接將類似 select {}
的空語句,轉(zhuǎn)換成對(duì) block
函數(shù)的調(diào)用:
func block() { gopark(nil, nil, waitReasonSelectNoCases, traceEvGoStop, 1) }
block
函數(shù)的實(shí)現(xiàn)非常簡(jiǎn)單,它會(huì)運(yùn)行 gopark
讓出當(dāng)前 Goroutine 對(duì)處理器的使用權(quán),該 Goroutine 也會(huì)進(jìn)入永久休眠的狀態(tài)也沒有辦法被其他的 Goroutine 喚醒,我們可以看到調(diào)用 gopark
方法時(shí)傳入的等待原因是 waitReasonSelectNoCases
,這其實(shí)也在告訴我們一個(gè)空的 select
語句會(huì)直接阻塞當(dāng)前的 Goroutine。
獨(dú)立情況
如果當(dāng)前的 select
條件只包含一個(gè) case
,那么就會(huì)就會(huì)執(zhí)行如下的優(yōu)化策略將原來的 select
語句改寫成 if
條件語句,下面是在 select
中從 Channel 接受數(shù)據(jù)時(shí)被改寫的情況:
select { case v, ok <-ch: // ... } if ch == nil { block() } v, ok := <-ch // ...
在 walkselectcases
函數(shù)中,如果只包含一個(gè)發(fā)送的 case
,那么就不會(huì)包含 v, ok := <- ch
這個(gè)表達(dá)式,因?yàn)橄?Channel 發(fā)送數(shù)據(jù)并沒有任何的返回值。
我們可以看到如果在 select
中僅存在一個(gè) case
,那么當(dāng) case
中處理的 Channel 是空指針時(shí),就會(huì)發(fā)生和沒有 case
的 select
語句一樣的情況,也就是直接掛起當(dāng)前 Goroutine 并且永遠(yuǎn)不會(huì)被喚醒。
非阻塞操作
在下一次的優(yōu)化策略執(zhí)行之前,walkselectcases
函數(shù)會(huì)先將 case
中所有 Channel 都轉(zhuǎn)換成指向 Channel 的地址以便于接下來的優(yōu)化和通用邏輯的執(zhí)行,改寫之后就會(huì)進(jìn)行最后一次的代碼優(yōu)化,觸發(fā)的條件就是 — select
中包含兩個(gè) case
,但是其中一個(gè)是 default
,我們可以分成發(fā)送和接收兩種情況介紹處理的過程。
發(fā)送
首先就是 Channel 的發(fā)送過程,也就是 case
中的表達(dá)式是 OSEND
類型,在這種情況下會(huì)使用 if/else
語句改寫代碼:
select { case ch <- i: // ... default: // ... } if selectnbsend(ch, i) { // ... } else { // ... }
這里最重要的函數(shù)其實(shí)就是 selectnbsend
,它的主要作用就是非阻塞地向 Channel 中發(fā)送數(shù)據(jù),我們?cè)?Channel 一節(jié)曾經(jīng)提到過發(fā)送數(shù)據(jù)的 chansend
函數(shù)包含一個(gè) block
參數(shù),這個(gè)參數(shù)會(huì)決定這一次的發(fā)送是不是阻塞的:
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) { return chansend(c, elem, false, getcallerpc()) }
在這里我們只需要知道當(dāng)前的發(fā)送過程不是阻塞的,哪怕是沒有接收方、緩沖區(qū)空間不足導(dǎo)致失敗了也會(huì)立即返回。
接收
由于從 Channel 中接收數(shù)據(jù)可能會(huì)返回一個(gè)或者兩個(gè)值,所以這里的情況會(huì)比發(fā)送時(shí)稍顯復(fù)雜,不過改寫的套路和邏輯確是差不多的:
select { case v <- ch: // case v, received <- ch: // ... default: // ... } if selectnbrecv(&v, ch) { // if selectnbrecv2(&v, &received, ch) { // ... } else { // ... }
返回值數(shù)量不同會(huì)導(dǎo)致最終使用函數(shù)的不同,兩個(gè)用于非阻塞接收消息的函數(shù) selectnbrecv
和 selectnbrecv2
其實(shí)只是對(duì) chanrecv
返回值的處理稍有不同:
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àn)榻邮辗讲恍枰?,所?nbsp;selectnbrecv
會(huì)直接忽略返回的布爾值,而 selectnbrecv2
會(huì)將布爾值回傳給上層;與 chansend
一樣,chanrecv
也提供了一個(gè) block
參數(shù)用于控制這一次接收是否阻塞。
通用情況
在默認(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
一個(gè)包含三個(gè) case
的正常 select
語句其實(shí)會(huì)被展開成如下所示的邏輯,我們可以看到其中處理的三個(gè)部分:
selv := [3]scase{} order := [6]uint16 for i, cas := range cases { c := scase{} c.kind = ... c.elem = ... c.c = ... } chosen, revcOK := selectgo(selv, order, 3) if chosen == 0 { // ... break } if chosen == 1 { // ... break } if chosen == 2 { // ... break }
展開后的 select
其實(shí)包含三部分,最開始初始化數(shù)組并轉(zhuǎn)換 scase
結(jié)構(gòu)體,使用 selectgo
選擇執(zhí)行的 case
以及最后通過 if
判斷選中的情況并執(zhí)行 case
中的表達(dá)式,需要注意的是這里其實(shí)也僅僅展開了 select
控制結(jié)構(gòu),select
語句執(zhí)行最重要的過程其實(shí)也是選擇 case
執(zhí)行的過程,這是我們?cè)谙乱还?jié)運(yùn)行時(shí)重點(diǎn)介紹的。
運(yùn)行時(shí)
我們已經(jīng)充分地了解了 select 在編譯期間的處理過程,接下來可以展開介紹 selectgo 函數(shù)的實(shí)現(xiàn)原理了。
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) { } selectgo 是會(huì)在運(yùn)行期間運(yùn)行的函數(shù),這個(gè)函數(shù)的主要作用就是從 select 控制結(jié)構(gòu)中的多個(gè) case 中選擇一個(gè)需要執(zhí)行的 case,隨后的多個(gè) if 條件語句就會(huì)根據(jù) selectgo 的返回值執(zhí)行相應(yīng)的語句。
初始化
selectgo
函數(shù)首先會(huì)進(jìn)行執(zhí)行必要的一些初始化操作,也就是決定處理 case
的兩個(gè)順序,其中一個(gè)是 pollOrder
另一個(gè)是 lockOrder
:
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) // ... }
Channel 的輪詢順序是通過 fastrandn
隨機(jī)生成的,這其實(shí)就導(dǎo)致了如果多個(gè) Channel 同時(shí)『響應(yīng)』,select
會(huì)隨機(jī)選擇其中的一個(gè)執(zhí)行;而另一個(gè) lockOrder
就是根據(jù) Channel 的地址確定的,根據(jù)相同的順序鎖定 Channel 能夠避免死鎖的發(fā)生,最后調(diào)用的 sellock
就會(huì)按照之前生成的順序鎖定所有的 Channel。
循環(huán)
當(dāng)我們?yōu)?nbsp;select
語句確定了輪詢和鎖定的順序并鎖定了所有的 Channel 之后就會(huì)開始進(jìn)入 select
的主循環(huán),查找或者等待 Channel 準(zhǔn)備就緒,循環(huán)中會(huì)遍歷所有的 case
并找到需要被喚起的 sudog
結(jié)構(gòu)體,在這段循環(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
— 當(dāng)前 case
表示默認(rèn)情況,如果循環(huán)執(zhí)行到了這種情況就表示前面的所有 case
都沒有被執(zhí)行,所以這里會(huì)直接解鎖所有的 Channel 并退出 selectgo
函數(shù),這時(shí)也就意味著當(dāng)前 select
結(jié)構(gòu)中的其他收發(fā)語句都是非阻塞的。
這其實(shí)是循環(huán)執(zhí)行的第一次遍歷,主要作用就是尋找所有 case
中 Channel 是否有可以立刻被處理的情況,無論是在包含等待的 Goroutine 還是緩沖區(qū)中存在數(shù)據(jù),只要滿足條件就會(huì)立刻處理,如果不能立刻找到活躍的 Channel 就會(huì)進(jìn)入循環(huán)的下一個(gè)過程,按照需要將當(dāng)前的 Goroutine 加入到所有 Channel 的 sendq
或者 recvq
隊(duì)列中:
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) // ... }
這里創(chuàng)建 sudog
并入隊(duì)的過程其實(shí)和 Channel 中直接進(jìn)行發(fā)送和接收時(shí)的過程幾乎完全相同,只是除了在入隊(duì)之外,這些 sudog
結(jié)構(gòu)體都會(huì)被串成鏈表附著在當(dāng)前 Goroutine 上,在入隊(duì)之后會(huì)調(diào)用 gopark
函數(shù)掛起當(dāng)前的 Goroutine 等待調(diào)度器的喚醒。
等到 select
對(duì)應(yīng)的一些 Channel 準(zhǔn)備好之后,當(dāng)前 Goroutine 就會(huì)被調(diào)度器喚醒,這時(shí)就會(huì)繼續(xù)執(zhí)行 selectgo
函數(shù)中剩下的邏輯,也就是從上面 入隊(duì)的 sudog
結(jié)構(gòu)體中獲取數(shù)據(jù):
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) { // ... gp.selectDone = 0 sg = (*sudog)(gp.param) gp.param = nil casi = -1 cas = nil sglist = gp.waiting gp.waiting = nil for _, casei := range lockorder { k = &scases[casei] if sg == sglist { casi = int(casei) cas = k } else { if k.kind == caseSend { c.sendq.dequeueSudoG(sglist) } else { c.recvq.dequeueSudoG(sglist) } } sgnext = sglist.waitlink sglist.waitlink = nil releaseSudog(sglist) sglist = sgnext } c = cas.c if cas.kind == caseRecv { recvOK = true } selunlock(scases, lockorder) goto retc // ... }
在第三次根據(jù) lockOrder
遍歷全部 case
的過程中,我們會(huì)先獲取 Goroutine 接收到的參數(shù) param
,這個(gè)參數(shù)其實(shí)就是被喚醒的 sudog
結(jié)構(gòu),我們會(huì)依次對(duì)比所有 case
對(duì)應(yīng)的 sudog
結(jié)構(gòu)找到被喚醒的 case
并釋放其他未被使用的 sudog
結(jié)構(gòu)。
由于當(dāng)前的 select
結(jié)構(gòu)已經(jīng)挑選了其中的一個(gè) case
進(jìn)行執(zhí)行,那么剩下 case
中沒有被用到的 sudog
其實(shí)就會(huì)直接忽略并且釋放掉了,為了不影響 Channel 的正常使用,我們還是需要將這些廢棄的 sudog
從 Channel 中出隊(duì);而除此之外的發(fā)生事件導(dǎo)致我們被喚醒的 sudog
結(jié)構(gòu)已經(jīng)在 Channel 進(jìn)行收發(fā)時(shí)就已經(jīng)出隊(duì)了,不需要我們?cè)俅翁幚恚鲫?duì)的代碼以及相關(guān)分析其實(shí)都在 Channel 一節(jié)中發(fā)送和接收的章節(jié)。
當(dāng)我們?cè)谘h(huán)中發(fā)現(xiàn)緩沖區(qū)中有元素或者緩沖區(qū)未滿時(shí)就會(huì)通過 goto
關(guān)鍵字跳轉(zhuǎn)到以下的兩個(gè)代碼段,這兩段代碼的執(zhí)行過程其實(shí)都非常簡(jiǎn)單,都只是向 Channel 中發(fā)送或者從緩沖區(qū)中直接獲取新的數(shù)據(jù):
bufrecv: recvOK = true qp = chanbuf(c, c.recvx) if cas.elem != nil { typedmemmove(c.elemtype, cas.elem, qp) } typedmemclr(c.elemtype, qp) c.recvx++ if c.recvx == c.dataqsiz { c.recvx = 0 } c.qcount-- selunlock(scases, lockorder) goto retc bufsend: typedmemmove(c.elemtype, chanbuf(c, c.sendx), cas.elem) c.sendx++ if c.sendx == c.dataqsiz { c.sendx = 0 } c.qcount++ selunlock(scases, lockorder) goto retc
這里在緩沖區(qū)中進(jìn)行的操作和直接對(duì) Channel 調(diào)用 chansend
和 chanrecv
進(jìn)行收發(fā)的過程差不多,執(zhí)行結(jié)束之后就會(huì)直接跳到 retc
字段。
兩個(gè)直接收發(fā)的情況,其實(shí)也就是調(diào)用 Channel 運(yùn)行時(shí)的兩個(gè)方法 send
和 recv
,這兩個(gè)方法會(huì)直接操作對(duì)應(yīng)的 Channel:
recv: recv(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2) recvOK = true goto retc send: send(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2) goto retc
不過當(dāng)發(fā)送或者接收時(shí),情況就稍微有一點(diǎn)復(fù)雜了,從一個(gè)關(guān)閉 Channel 中接收數(shù)據(jù)會(huì)直接清除 Channel 中的相關(guān)內(nèi)容,而向一個(gè)關(guān)閉的 Channel 發(fā)送數(shù)據(jù)就會(huì)直接 panic
造成程序崩潰:
rclose: selunlock(scases, lockorder) recvOK = false if cas.elem != nil { typedmemclr(c.elemtype, cas.elem) } goto retc sclose: selunlock(scases, lockorder) panic(plainError("send on closed channel"))
總體來看,Channel 相關(guān)的收發(fā)操作和上一節(jié) Channel 實(shí)現(xiàn)原理中介紹的沒有太多出入,只是由于 select
多出了 default
關(guān)鍵字所以會(huì)出現(xiàn)非阻塞收發(fā)的情況。
總結(jié)
到這一節(jié)的最后我們需要總結(jié)一下,select
結(jié)構(gòu)的執(zhí)行過程與實(shí)現(xiàn)原理,首先在編譯期間,Go 語言會(huì)對(duì) select
語句進(jìn)行優(yōu)化,以下是根據(jù) select
中語句的不同選擇了不同的優(yōu)化路徑:
空的 select
語句會(huì)被直接轉(zhuǎn)換成 block
函數(shù)的調(diào)用,直接掛起當(dāng)前 Goroutine;
如果 select
語句中只包含一個(gè) case
,就會(huì)被轉(zhuǎn)換成 if ch == nil { block }; n;
表達(dá)式;
- 首先判斷操作的 Channel 是不是空的;
- 然后執(zhí)行
case
結(jié)構(gòu)中的內(nèi)容;
如果 select
語句中只包含兩個(gè) case
并且其中一個(gè)是 default
,那么 Channel 和接收和發(fā)送操作都會(huì)使用 selectnbrecv
和 selectnbsend
非阻塞地執(zhí)行接收和發(fā)送操作;
在默認(rèn)情況下會(huì)通過 selectgo
函數(shù)選擇需要執(zhí)行的 case
并通過多個(gè) if
語句執(zhí)行 case
中的表達(dá)式;
在編譯器已經(jīng)對(duì) select
語句進(jìn)行優(yōu)化之后,Go 語言會(huì)在運(yùn)行時(shí)執(zhí)行編譯期間展開的 selectgo
函數(shù),這個(gè)函數(shù)會(huì)按照以下的過程執(zhí)行:
1.隨機(jī)生成一個(gè)遍歷的輪詢順序 pollOrder
并根據(jù) Channel 地址生成一個(gè)用于遍歷的鎖定順序 lockOrder
;
2.根據(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)度;
3.當(dāng)調(diào)度器喚醒當(dāng)前 Goroutine 時(shí)就會(huì)再次按照 lockOrder
遍歷所有的 case
,從中查找需要被處理的 sudog
結(jié)構(gòu)并返回對(duì)應(yīng)的索引;
然而并不是所有的 select
控制結(jié)構(gòu)都會(huì)走到 selectgo
上,很多情況都會(huì)被直接優(yōu)化掉,沒有機(jī)會(huì)調(diào)用 selectgo
函數(shù)。
Go 語言中的 select
關(guān)鍵字與 IO 多路復(fù)用中的 select
、epoll
等函數(shù)非常相似,不但 Channel 的收發(fā)操作與等待 IO 的讀寫能找到這種一一對(duì)應(yīng)的關(guān)系,這兩者的作用也非常相似;總的來說,select
關(guān)鍵字的實(shí)現(xiàn)原理稍顯復(fù)雜,與 Channel 的關(guān)系非常緊密,這里省略了很多 Channel 操作的細(xì)節(jié),數(shù)據(jù)結(jié)構(gòu)一章其實(shí)就介紹了 Channel 收發(fā)的相關(guān)細(xì)節(jié)。
到此這篇關(guān)于一文帶你了解Golang中select的實(shí)現(xiàn)原理的文章就介紹到這了,更多相關(guān)Golang select內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言Elasticsearch數(shù)據(jù)清理工具思路詳解
這篇文章主要介紹了Go語言Elasticsearch數(shù)據(jù)清理工具思路詳解,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-10-10夯實(shí)Golang基礎(chǔ)之?dāng)?shù)據(jù)類型梳理匯總
這篇文章主要8為大家介紹了夯實(shí)Golang基礎(chǔ)之?dāng)?shù)據(jù)類型梳理匯總,有需要的朋友可以借鑒參考下,希望能夠有所幫助2023-10-10淺談beego默認(rèn)處理靜態(tài)文件性能低下的問題
下面小編就為大家?guī)硪黄獪\談beego默認(rèn)處理靜態(tài)文件性能低下的問題。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-06-06Go+Kafka實(shí)現(xiàn)延遲消息的實(shí)現(xiàn)示例
本文主要介紹了Go+Kafka實(shí)現(xiàn)延遲消息的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07Go方法簡(jiǎn)單性和高效性的充分體現(xiàn)詳解
本文深入探討了Go語言中方法的各個(gè)方面,包括基礎(chǔ)概念、定義與聲明、特性、實(shí)戰(zhàn)應(yīng)用以及性能考量,文章充滿技術(shù)深度,通過實(shí)例和代碼演示,力圖幫助讀者全面理解Go方法的設(shè)計(jì)哲學(xué)和最佳實(shí)踐2023-10-10Golang語言中的Prometheus的日志模塊使用案例代碼編寫
這篇文章主要介紹了Golang語言中的Prometheus的日志模塊使用案例,本文給大家分享源代碼編寫方法,感興趣的朋友跟隨小編一起看看吧2024-08-08go語言開發(fā)環(huán)境安裝及第一個(gè)go程序(推薦)
這篇文章主要介紹了go語言開發(fā)環(huán)境安裝及第一個(gè)go程序,這篇通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-02-02Go使用Protocol?Buffers在數(shù)據(jù)序列化的優(yōu)勢(shì)示例詳解
這篇文章主要為大家介紹了Go使用Protocol?Buffers在數(shù)據(jù)序列化的優(yōu)勢(shì)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11