一文帶你了解Golang中select的實(shí)現(xiàn)原理
概述
select是go提供的一種跟并發(fā)相關(guān)的語(yǔ)法,非常有用。本文將介紹 Go 語(yǔ)言中的 select 的實(shí)現(xiàn)原理,包括 select 的結(jié)構(gòu)和常見問(wèn)題、編譯期間的多種優(yōu)化以及運(yùn)行時(shí)的執(zhí)行過(guò)程。

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è)的返回,無(wú)論哪一個(gè)返回都會(huì)立刻執(zhí)行 case 中的代碼,不過(guò)如果了 select 中的兩個(gè) case 同時(shí)被觸發(fā),就會(huì)隨機(jī)選擇一個(gè) case 執(zhí)行。
結(jié)構(gòu)
select 在 Go 語(yǔ)言的源代碼中其實(shí)不存在任何的結(jié)構(gòu)體表示,但是 select 控制結(jié)構(gòu)中 case 卻使用了 scase 結(jié)構(gòu)體來(lái)表示:
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 語(yǔ)言中使用 select 控制結(jié)構(gòu)時(shí),其實(shí)會(huì)遇到兩個(gè)非常有趣的問(wèn)題,一個(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)行選擇的問(wèn)題,我們通過(guò)下面的代碼可以簡(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 語(yǔ)句在編譯期間會(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 語(yǔ)句在編譯期間進(jìn)行的優(yōu)化和轉(zhuǎn)換。
編譯器在中間代碼生成期間會(huì)根據(jù) select 中 case 的不同對(duì)控制語(yǔ)句進(jìn)行優(yōu)化,這一過(guò)程其實(shí)都發(fā)生在 walkselectcases 函數(shù)中,我們?cè)谶@里會(huì)分四種情況分別介紹優(yōu)化的過(guò)程和結(jié)果:
select 中不存在任何的 case;
select 中只存在一個(gè) case;
select 中存在兩個(gè) case,其中一個(gè) case 是 default 語(yǔ)句;
通用的 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 {} 的空語(yǔ)句,轉(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)也沒(méi)有辦法被其他的 Goroutine 喚醒,我們可以看到調(diào)用 gopark 方法時(shí)傳入的等待原因是 waitReasonSelectNoCases,這其實(shí)也在告訴我們一個(gè)空的 select 語(yǔ)句會(huì)直接阻塞當(dāng)前的 Goroutine。
獨(dú)立情況
如果當(dāng)前的 select 條件只包含一個(gè) case,那么就會(huì)就會(huì)執(zhí)行如下的優(yōu)化策略將原來(lái)的 select 語(yǔ)句改寫成 if 條件語(yǔ)句,下面是在 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ù)并沒(méi)有任何的返回值。
我們可以看到如果在 select 中僅存在一個(gè) case,那么當(dāng) case 中處理的 Channel 是空指針時(shí),就會(huì)發(fā)生和沒(méi)有 case 的 select 語(yǔ)句一樣的情況,也就是直接掛起當(dāng)前 Goroutine 并且永遠(yuǎn)不會(huì)被喚醒。
非阻塞操作
在下一次的優(yōu)化策略執(zhí)行之前,walkselectcases 函數(shù)會(huì)先將 case 中所有 Channel 都轉(zhuǎn)換成指向 Channel 的地址以便于接下來(lái)的優(yōu)化和通用邏輯的執(zhí)行,改寫之后就會(huì)進(jìn)行最后一次的代碼優(yōu)化,觸發(fā)的條件就是 — select 中包含兩個(gè) case,但是其中一個(gè)是 default,我們可以分成發(fā)送和接收兩種情況介紹處理的過(guò)程。
發(fā)送
首先就是 Channel 的發(fā)送過(guò)程,也就是 case 中的表達(dá)式是 OSEND 類型,在這種情況下會(huì)使用 if/else 語(yǔ)句改寫代碼:
select {
case ch <- i:
// ...
default:
// ...
}
if selectnbsend(ch, i) {
// ...
} else {
// ...
}這里最重要的函數(shù)其實(shí)就是 selectnbsend,它的主要作用就是非阻塞地向 Channel 中發(fā)送數(shù)據(jù),我們?cè)?Channel 一節(jié)曾經(jīng)提到過(guò)發(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ā)送過(guò)程不是阻塞的,哪怕是沒(méi)有接收方、緩沖區(qū)空間不足導(dǎo)致失敗了也會(huì)立即返回。
接收
由于從 Channel 中接收數(shù)據(jù)可能會(huì)返回一個(gè)或者兩個(gè)值,所以這里的情況會(huì)比發(fā)送時(shí)稍顯復(fù)雜,不過(guò)改寫的套路和邏輯確是差不多的:
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 語(yǔ)句會(huì)在編譯階段經(jīng)過(guò)如下過(guò)程的處理:
- 將所有的
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是否是接收的布爾值; - 通過(guò)
for循環(huán)生成一組if語(yǔ)句,在語(yǔ)句中判斷自己是不是被選中的case
一個(gè)包含三個(gè) case 的正常 select 語(yǔ)句其實(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 以及最后通過(guò) if 判斷選中的情況并執(zhí)行 case 中的表達(dá)式,需要注意的是這里其實(shí)也僅僅展開了 select 控制結(jié)構(gòu),select 語(yǔ)句執(zhí)行最重要的過(guò)程其實(shí)也是選擇 case 執(zhí)行的過(guò)程,這是我們?cè)谙乱还?jié)運(yùn)行時(shí)重點(diǎn)介紹的。
運(yùn)行時(shí)
我們已經(jīng)充分地了解了 select 在編譯期間的處理過(guò)程,接下來(lái)可以展開介紹 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 條件語(yǔ)句就會(huì)根據(jù) selectgo 的返回值執(zhí)行相應(yīng)的語(yǔ)句。
初始化
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 的輪詢順序是通過(guò) 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 語(yǔ)句確定了輪詢和鎖定的順序并鎖定了所有的 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ì)被跳過(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 — 當(dāng)前 case 表示默認(rèn)情況,如果循環(huán)執(zhí)行到了這種情況就表示前面的所有 case 都沒(méi)有被執(zhí)行,所以這里會(huì)直接解鎖所有的 Channel 并退出 selectgo 函數(shù),這時(shí)也就意味著當(dāng)前 select 結(jié)構(gòu)中的其他收發(fā)語(yǔ)句都是非阻塞的。

這其實(shí)是循環(huán)執(zhí)行的第一次遍歷,主要作用就是尋找所有 case 中 Channel 是否有可以立刻被處理的情況,無(wú)論是在包含等待的 Goroutine 還是緩沖區(qū)中存在數(shù)據(jù),只要滿足條件就會(huì)立刻處理,如果不能立刻找到活躍的 Channel 就會(huì)進(jìn)入循環(huán)的下一個(gè)過(guò)程,按照需要將當(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ì)的過(guò)程其實(shí)和 Channel 中直接進(jìn)行發(fā)送和接收時(shí)的過(guò)程幾乎完全相同,只是除了在入隊(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 的過(guò)程中,我們會(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 中沒(méi)有被用到的 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ì)通過(guò) goto 關(guān)鍵字跳轉(zhuǎn)到以下的兩個(gè)代碼段,這兩段代碼的執(zhí)行過(guò)程其實(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ā)的過(guò)程差不多,執(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不過(guò)當(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"))
總體來(lái)看,Channel 相關(guān)的收發(fā)操作和上一節(jié) Channel 實(shí)現(xiàn)原理中介紹的沒(méi)有太多出入,只是由于 select 多出了 default 關(guān)鍵字所以會(huì)出現(xiàn)非阻塞收發(fā)的情況。
總結(jié)
到這一節(jié)的最后我們需要總結(jié)一下,select 結(jié)構(gòu)的執(zhí)行過(guò)程與實(shí)現(xiàn)原理,首先在編譯期間,Go 語(yǔ)言會(huì)對(duì) select 語(yǔ)句進(jìn)行優(yōu)化,以下是根據(jù) select 中語(yǔ)句的不同選擇了不同的優(yōu)化路徑:
空的 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á)式;
- 首先判斷操作的 Channel 是不是空的;
- 然后執(zhí)行
case結(jié)構(gòu)中的內(nèi)容;
如果 select 語(yǔ)句中只包含兩個(gè) case 并且其中一個(gè)是 default,那么 Channel 和接收和發(fā)送操作都會(huì)使用 selectnbrecv 和 selectnbsend 非阻塞地執(zhí)行接收和發(fā)送操作;
在默認(rèn)情況下會(huì)通過(guò) selectgo 函數(shù)選擇需要執(zhí)行的 case 并通過(guò)多個(gè) if 語(yǔ)句執(zhí)行 case 中的表達(dá)式;
在編譯器已經(jīng)對(duì) select 語(yǔ)句進(jìn)行優(yōu)化之后,Go 語(yǔ)言會(huì)在運(yùn)行時(shí)執(zhí)行編譯期間展開的 selectgo 函數(shù),這個(gè)函數(shù)會(huì)按照以下的過(guò)程執(zhí)行:
1.隨機(jī)生成一個(gè)遍歷的輪詢順序 pollOrder 并根據(jù) Channel 地址生成一個(gè)用于遍歷的鎖定順序 lockOrder;
2.根據(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)度;
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)化掉,沒(méi)有機(jī)會(huì)調(diào)用 selectgo 函數(shù)。
Go 語(yǔ)言中的 select 關(guān)鍵字與 IO 多路復(fù)用中的 select、epoll 等函數(shù)非常相似,不但 Channel 的收發(fā)操作與等待 IO 的讀寫能找到這種一一對(duì)應(yīng)的關(guān)系,這兩者的作用也非常相似;總的來(lái)說(shuō),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語(yǔ)言Elasticsearch數(shù)據(jù)清理工具思路詳解
這篇文章主要介紹了Go語(yǔ)言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)文件性能低下的問(wèn)題
下面小編就為大家?guī)?lái)一篇淺談beego默認(rèn)處理靜態(tài)文件性能低下的問(wèn)題。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-06-06
Go+Kafka實(shí)現(xiàn)延遲消息的實(shí)現(xiàn)示例
本文主要介紹了Go+Kafka實(shí)現(xiàn)延遲消息的實(shí)現(xiàn)示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07
Go方法簡(jiǎn)單性和高效性的充分體現(xiàn)詳解
本文深入探討了Go語(yǔ)言中方法的各個(gè)方面,包括基礎(chǔ)概念、定義與聲明、特性、實(shí)戰(zhàn)應(yīng)用以及性能考量,文章充滿技術(shù)深度,通過(guò)實(shí)例和代碼演示,力圖幫助讀者全面理解Go方法的設(shè)計(jì)哲學(xué)和最佳實(shí)踐2023-10-10
Golang語(yǔ)言中的Prometheus的日志模塊使用案例代碼編寫
這篇文章主要介紹了Golang語(yǔ)言中的Prometheus的日志模塊使用案例,本文給大家分享源代碼編寫方法,感興趣的朋友跟隨小編一起看看吧2024-08-08
go語(yǔ)言開發(fā)環(huán)境安裝及第一個(gè)go程序(推薦)
這篇文章主要介紹了go語(yǔ)言開發(fā)環(huán)境安裝及第一個(gè)go程序,這篇通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-02-02
Go使用Protocol?Buffers在數(shù)據(jù)序列化的優(yōu)勢(shì)示例詳解
這篇文章主要為大家介紹了Go使用Protocol?Buffers在數(shù)據(jù)序列化的優(yōu)勢(shì)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11

