詳解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)聽(tīng)多個(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)聽(tīng)多個(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)聽(tīng),如果兩個(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)文章
golang 實(shí)現(xiàn)tcp server端和client端,并計(jì)算RTT時(shí)間操作
這篇文章主要介紹了golang 實(shí)現(xiàn)tcp server端和client端,并計(jì)算RTT時(shí)間操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-12-12Go語(yǔ)言實(shí)現(xiàn)釘釘發(fā)送通知
本文通過(guò)代碼給大家介紹了Go語(yǔ)言實(shí)現(xiàn)釘釘發(fā)送通知,代碼簡(jiǎn)單易懂,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-11-11如何組織Go代碼目錄結(jié)構(gòu)依賴(lài)注入wire使用解析
這篇文章主要為大家介紹了如何組織Go代碼目錄結(jié)構(gòu)依賴(lài)注入wire使用解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07Go編寫(xiě)定時(shí)器與定時(shí)任務(wù)詳解(附第三方庫(kù)gocron用法)
當(dāng)需要每天執(zhí)行定時(shí)任務(wù)的時(shí)候就需要定時(shí)器來(lái)處理了,周期任務(wù),倒計(jì)時(shí)任務(wù),定點(diǎn)任務(wù)等,下面這篇文章主要給大家介紹了關(guān)于Go編寫(xiě)定時(shí)器與定時(shí)任務(wù)的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07