詳解Golang中select的使用與源碼分析
背景
golang 中主推 channel 通信。單個(gè) channel 的通信可以通過(guò)一個(gè)goroutine往 channel 發(fā)數(shù)據(jù),另外一個(gè)從channel取數(shù)據(jù)進(jìn)行。這是阻塞的,因?yàn)橐腠樌麍?zhí)行完這個(gè)步驟,需要 channel 準(zhǔn)備好才行,準(zhǔn)備好的條件如下:
1.發(fā)送
- 緩存有空間(如果是有緩存的
channel) - 有等待接收的
goroutine
2.接收
- 緩存有數(shù)據(jù)(如果是有緩存的
channel) - 有等待發(fā)送的
goroutine
對(duì)channel實(shí)際使用中還有如下兩個(gè)需求,這個(gè)時(shí)候就需要select了。
- 同時(shí)監(jiān)聽多個(gè)
channel - 在沒(méi)有
channel準(zhǔn)備好的時(shí)候,也可以往下執(zhí)行。
select 流程
1.空select。作用是阻塞當(dāng)前goroutine。不要用for{}來(lái)阻塞goroutine,因?yàn)闀?huì)占用cpu。而select{}不會(huì),因?yàn)楫?dāng)前goroutine不會(huì)再被調(diào)度。
if len(cases) == 0 {
block()
}2.配置好poll的順序。由于是同時(shí)監(jiān)聽多個(gè)channel的發(fā)送或者接收,所以需要按照一定的順序查看哪個(gè)channel準(zhǔn)備好了。如果每次采用select中的順序查看channel是否準(zhǔn)備好了,那么只要在前面的channel準(zhǔn)備好的足夠快,那么會(huì)造成后面的channel即使準(zhǔn)備好了,也永遠(yuǎn)不會(huì)被執(zhí)行。打亂順序的邏輯如下,采用了洗牌算法\color{red}{洗牌算法}洗牌算法,注意此過(guò)程中會(huì)過(guò)濾掉channel為nil的case。\color{red}{注意此過(guò)程中會(huì)過(guò)濾掉 channel 為 nil 的 case。}注意此過(guò)程中會(huì)過(guò)濾掉channel為nil的case。
// generate permuted order
norder := 0
for i := range scases {
cas := &scases[i]
// Omit cases without channels from the poll and lock orders.
if cas.c == nil {
cas.elem = nil // allow GC
continue
}
j := fastrandn(uint32(norder + 1))
pollorder[norder] = pollorder[j]
pollorder[j] = uint16(i)
norder++
}3.配置好lock的順序。由于可能會(huì)修改channel中的數(shù)據(jù),所以在打算往channel中發(fā)送數(shù)據(jù)或者從channel接收數(shù)據(jù)的時(shí)候,需要鎖住 channel。而一個(gè)channel可能被多個(gè)select監(jiān)聽,如果兩個(gè)select對(duì)兩個(gè)channel A和B,分別按照順序A, B和B,A上鎖,是可能會(huì)造成死鎖的,導(dǎo)致兩個(gè)select都執(zhí)行不下去。

所以select中鎖住channel的順序至關(guān)重要,解決方案是按照channel的地址的順序鎖住channel。因?yàn)樵趦蓚€(gè)select中channel有交集的時(shí)候,都是按照交集中channel的地址順序鎖channel。
實(shí)際排序代碼如下,采用堆排序算法\color{red}{堆排序算法}堆排序算法按照channel的地址從小到大對(duì)channel進(jìn)行排序。
// sort the cases by Hchan address to get the locking order.
// simple heap sort, to guarantee n log n time and constant stack footprint.
for i := range lockorder {
j := i
// Start with the pollorder to permute cases on the same channel.
c := scases[pollorder[i]].c
for j > 0 && scases[lockorder[(j-1)/2]].c.sortkey() < c.sortkey() {
k := (j - 1) / 2
lockorder[j] = lockorder[k]
j = k
}
lockorder[j] = pollorder[i]
}
for i := len(lockorder) - 1; i >= 0; i-- {
o := lockorder[i]
c := scases[o].c
lockorder[i] = lockorder[0]
j := 0
for {
k := j*2 + 1
if k >= i {
break
}
if k+1 < i && scases[lockorder[k]].c.sortkey() < scases[lockorder[k+1]].c.sortkey() {
k++
}
if c.sortkey() < scases[lockorder[k]].c.sortkey() {
lockorder[j] = lockorder[k]
j = k
continue
}
break
}
lockorder[j] = o
}4.鎖住select中的所有channel。要查看channel中的數(shù)據(jù)了。
// lock all the channels involved in the select sellock(scases, lockorder)
5.第一輪查看是否已有準(zhǔn)備好的channel。如果有直接發(fā)送數(shù)據(jù)到channel或者從channel接收數(shù)據(jù)。注意select的channel切片中,前面部分是從channel接收數(shù)據(jù)的case,后半部分是往channel發(fā)送數(shù)據(jù)的case。

按照pollorder順序查看是否有channel準(zhǔn)備好了。
for _, casei := range pollorder {
casi = int(casei)
cas = &scases[casi]
c = cas.c
if casi >= nsends {
sg = c.sendq.dequeue()
if sg != nil {
goto recv
}
if c.qcount > 0 {
goto bufrecv
}
if c.closed != 0 {
goto rclose
}
} else {
if raceenabled {
racereadpc(c.raceaddr(), casePC(casi), chansendpc)
}
if c.closed != 0 {
goto sclose
}
sg = c.recvq.dequeue()
if sg != nil {
goto send
}
if c.qcount < c.dataqsiz {
goto bufsend
}
}
}6.直接執(zhí)行default分支
if !block {
selunlock(scases, lockorder)
casi = -1
goto retc
}7.第二輪遍歷channel。創(chuàng)建sudog把當(dāng)前goroutine放到每個(gè)channel的等待列表中去,等待channel準(zhǔn)備好時(shí)被喚醒。
// pass 2 - enqueue on all chans
gp = getg()
if gp.waiting != nil {
throw("gp.waiting != nil")
}
nextp = &gp.waiting
for _, casei := range lockorder {
casi = int(casei)
cas = &scases[casi]
c = cas.c
sg := acquireSudog()
sg.g = gp
sg.isSelect = true
// No stack splits between assigning elem and enqueuing
// sg on gp.waiting where copystack can find it.
sg.elem = cas.elem
sg.releasetime = 0
if t0 != 0 {
sg.releasetime = -1
}
sg.c = c
// Construct waiting list in lock order.
*nextp = sg
nextp = &sg.waitlink
if casi < nsends {
c.sendq.enqueue(sg)
} else {
c.recvq.enqueue(sg)
}
}8.等待被喚醒。其中gopark的時(shí)候會(huì)釋放對(duì)所有channel占用的鎖。
// wait for someone to wake us up gp.param = nil // Signal to anyone trying to shrink our stack that we're about // to park on a channel. The window between when this G's status // changes and when we set gp.activeStackChans is not safe for // stack shrinking. atomic.Store8(&gp.parkingOnChan, 1) gopark(selparkcommit, nil, waitReasonSelect, traceEvGoBlockSelect, 1) gp.activeStackChans = false
9.被喚醒
- 鎖住所有
channel - 清理當(dāng)前
goroutine的等待sudog - 找到是被哪個(gè)
channel喚醒的,并清理每個(gè)channel上當(dāng)前的goroutine對(duì)應(yīng)的sudog
sellock(scases, lockorder)
gp.selectDone = 0
sg = (*sudog)(gp.param)
gp.param = nil
// pass 3 - dequeue from unsuccessful chans
// otherwise they stack up on quiet channels
// record the successful case, if any.
// We singly-linked up the SudoGs in lock order.
casi = -1
cas = nil
caseSuccess = false
sglist = gp.waiting
// Clear all elem before unlinking from gp.waiting.
for sg1 := gp.waiting; sg1 != nil; sg1 = sg1.waitlink {
sg1.isSelect = false
sg1.elem = nil
sg1.c = nil
}
gp.waiting = nil
for _, casei := range lockorder {
k = &scases[casei]
if sg == sglist {
// sg has already been dequeued by the G that woke us up.
casi = int(casei)
cas = k
caseSuccess = sglist.success
if sglist.releasetime > 0 {
caseReleaseTime = sglist.releasetime
}
} else {
c = k.c
if int(casei) < nsends {
c.sendq.dequeueSudoG(sglist)
} else {
c.recvq.dequeueSudoG(sglist)
}
}
sgnext = sglist.waitlink
sglist.waitlink = nil
releaseSudog(sglist)
sglist = sgnext
}到此這篇關(guān)于詳解Golang中select的使用與源碼分析的文章就介紹到這了,更多相關(guān)Golang select內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
以alpine作為基礎(chǔ)鏡像構(gòu)建Golang可執(zhí)行程序操作
這篇文章主要介紹了以alpine作為基礎(chǔ)鏡像構(gòu)建Golang可執(zhí)行程序操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-12-12
golang時(shí)間及時(shí)間戳的獲取轉(zhuǎn)換
本文主要介紹了golang時(shí)間及時(shí)間戳的獲取轉(zhuǎn)換,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06
Golang基于內(nèi)存的鍵值存儲(chǔ)緩存庫(kù)go-cache
go-cache是一個(gè)內(nèi)存中的key:value store/cache庫(kù),適用于單機(jī)應(yīng)用程序,本文主要介紹了Golang基于內(nèi)存的鍵值存儲(chǔ)緩存庫(kù)go-cache,具有一定的參考價(jià)值,感興趣的可以了解一下2025-03-03
Golang編程實(shí)現(xiàn)刪除字符串中出現(xiàn)次數(shù)最少字符的方法
這篇文章主要介紹了Golang編程實(shí)現(xiàn)刪除字符串中出現(xiàn)次數(shù)最少字符的方法,涉及Go語(yǔ)言字符串遍歷與運(yùn)算相關(guān)操作技巧,需要的朋友可以參考下2017-01-01
詳解Go語(yǔ)言如何利用上下文進(jìn)行并發(fā)計(jì)算
在Go編程中,上下文(context)是一個(gè)非常重要的概念,它包含了與請(qǐng)求相關(guān)的信息,本文主要來(lái)和大家討論一下如何在并發(fā)計(jì)算中使用上下文,感興趣的可以了解下2024-02-02
利用go語(yǔ)言實(shí)現(xiàn)Git?重命名遠(yuǎn)程分支??
這篇文章主要介紹了go語(yǔ)言實(shí)現(xiàn)Git?重命名遠(yuǎn)程分支,文章基于go語(yǔ)言的基礎(chǔ)展開Git?重命名遠(yuǎn)程分支的實(shí)現(xiàn)過(guò)程,需要的小伙伴可以參考一下,希望對(duì)你的學(xué)習(xí)有所幫助2022-06-06
Go語(yǔ)言實(shí)現(xiàn)的排列組合問(wèn)題實(shí)例(n個(gè)數(shù)中取m個(gè))
這篇文章主要介紹了Go語(yǔ)言實(shí)現(xiàn)的排列組合問(wèn)題,結(jié)合實(shí)例形式分析了Go語(yǔ)言實(shí)現(xiàn)排列組合數(shù)學(xué)運(yùn)算的原理與具體操作技巧,需要的朋友可以參考下2017-02-02

