本文由 簡悅 SimpRead 轉(zhuǎn)碼良狈, 原文地址 https://hitzhangjie.github.io/jekyll/update/2018/05/19/golang-select-case%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6.html
在介紹 select-case 實現(xiàn)機制之前就斤,最好先了解下 chan 操作規(guī)則澎灸,明白 goroutine 何時阻塞耿芹,又在什么時機被喚醒院水,這對后續(xù)理解 select-case 實現(xiàn)有幫助入蛆。所以接下來先介紹 chan 操作規(guī)則归敬,然后再介紹 select-case 的實現(xiàn)酷含。
1.1 chan 操作規(guī)則 1
當一個 goroutine 要從一個 non-nil & non-closed chan 上接收數(shù)據(jù)時,goroutine 首先會去獲取 chan 上的鎖汪茧,然后執(zhí)行如下操作直到某個條件被滿足:
1)如果 chan 上的 value buffer 不空椅亚,這也意味著 chan 上的 recv goroutine queue 也一定是空的,該接收 goroutine 將從 value buffer 中 unshift 出一個 value舱污。這個時候呀舔,如果 send goroutine 隊列不空的情況下,因為剛才 value buffer 中空出了一個位置扩灯,有位置可寫媚赖,所以這個時候會從 send goroutine queue 中 unshift 出一個發(fā)送 goroutine 并讓其恢復執(zhí)行,讓其執(zhí)行把數(shù)據(jù)寫入 chan 的操作珠插,實際上是恢復該發(fā)送該 goroutine 執(zhí)行惧磺,并把該發(fā)送 goroutine 要發(fā)送的數(shù)據(jù) push 到 value buffer 中。然后呢捻撑,該接收 goroutine 也拿到了數(shù)據(jù)了磨隘,就繼續(xù)執(zhí)行。這種情景顾患,channel 的接收操作稱為 non-blocking 操作番捂。
2)另一種情況,如果 value buffer 是空的描验,但是 send goroutine queue 不空白嘁,這種情況下,該 chan 一定是 unbufferred chan膘流,不然 value buffer 肯定有數(shù)據(jù)嘛絮缅,這個時候接收 goroutine 將從 send goroutine queue 中 unshift 出一個發(fā)送 goroutine鲁沥,并將該發(fā)送 goroutine 要發(fā)送的數(shù)據(jù)接收過來(兩個 goroutine 一個有發(fā)送數(shù)據(jù)地址,一個有接收數(shù)據(jù)地址耕魄,拷貝過來就 ok)画恰,然后這個取出的發(fā)送 goroutine 將恢復執(zhí)行,這個接收 goroutine 也可以繼續(xù)執(zhí)行吸奴。這種情況下允扇,chan 接收操作也是 non-blocking 操作。
3)另一種情況则奥,如果 value buffer 和 send goroutine queue 都是空的考润,沒有數(shù)據(jù)可接收,將把該接收 goroutine push 到 chan 的 recv goroutine queue读处,該接收 goroutine 將轉(zhuǎn)入 blocking 狀態(tài)糊治,什么時候恢復期執(zhí)行呢,要等到有一個 goroutine 嘗試向 chan 發(fā)送數(shù)據(jù)的時候了罚舱。這種場景下井辜,chan 接收操作是 blocking 操作。
1.2 chan 操作規(guī)則 2
當一個 goroutine 常識向一個 non-nil & non-closed chan 發(fā)送數(shù)據(jù)的時候管闷,該 goroutine 將先嘗試獲取 chan 上的鎖粥脚,然后執(zhí)行如下操作直到滿足其中一種情況。
1)如果 chan 的 recv goroutine queue 不空包个,這種情況下刷允,value buffer 一定是空的。發(fā)送 goroutine 將從 recv goroutine queue 中 unshift 出一個 recv goroutine赃蛛,然后直接將自己要發(fā)送的數(shù)據(jù)拷貝到該 recv goroutine 的接收地址處恃锉,然后恢復該 recv goroutine 的運行,當前發(fā)送 goroutine 也繼續(xù)執(zhí)行呕臂。這種情況下破托,chan send 操作是 non-blocking 操作。
2)如果 chan 的 recv goroutine queue 是空的歧蒋,并且 value buffer 不滿土砂,這種情況下,send goroutine queue 一定是空的谜洽,因為 value buffer 不滿發(fā)送 goroutine 可以發(fā)送完成不可能會阻塞萝映。該發(fā)送 goroutine 將要發(fā)送的數(shù)據(jù) push 到 value buffer 中然后繼續(xù)執(zhí)行。這種情況下阐虚,chan send 操作是 non-blocking 操作序臂。
3)如果 chan 的 recv goroutine queue 是空的,并且 value buffer 是滿的,發(fā)送 goroutine 將被 push 到 send goroutine queue 中進入阻塞狀態(tài)奥秆。等到有其他 goroutine 嘗試從 chan 接收數(shù)據(jù)的時候才能將其喚醒恢復執(zhí)行逊彭。這種情況下,chan send 操作是 blocking 操作构订。
1.3 chan 操作規(guī)則 3
當一個 goroutine 嘗試 close 一個 non-nil & non-closed chan 的時候侮叮,close 操作將依次執(zhí)行如下操作。
1)如果 chan 的 recv goroutine queue 不空悼瘾,這種情況下 value buffer 一定是空的囊榜,因為如果 value buffer 如果不空,一定會繼續(xù) unshift recv goroutine queue 中的 goroutine 接收數(shù)據(jù)亥宿,直到 value buffer 為空(這里可以看下 chan send 操作卸勺,chan send 寫入數(shù)據(jù)之前,一定會從 recv goroutine queue 中 unshift 出一個 recv goroutine)箩绍。recv goroutine queue 里面所有的 goroutine 將一個個 unshift 出來并返回一個 val=0 值和 sentBeforeClosed=false孔庭。
2)如果 chan 的 send goroutine queue 不空,所有的 goroutine 將被依次取出并生成一個 panic for closing a close chan材蛛。在這 close 之前發(fā)送到 chan 的數(shù)據(jù)仍然在 chan 的 value buffer 中存著。
1.4 chan 操作規(guī)則 4
一旦 chan 被關閉了怎抛,chan recv 操作就永遠也不會阻塞卑吭,chan 的 value buffer 中在 close 之前寫入的數(shù)據(jù)仍然存在。一旦 value buffer 中 close 之前寫入的數(shù)據(jù)都被取出之后马绝,后續(xù)的接收操作將會返回 val=0 和 sentBeforeClosed=true豆赏。
理解這里的 goroutine 的 blocking、non-blocking 操作對于理解針對 chan 的 select-case 操作是很有幫助的富稻。下面介紹 select-case 實現(xiàn)機制掷邦。
select-case 中假如沒有 default 分支的話,一定要等到某個 case 分支滿足條件然后將對應的 goroutine 喚醒恢復執(zhí)行才可以繼續(xù)執(zhí)行椭赋,否則代碼就會阻塞在這里抚岗,即將當前 goroutine push 到各個 case 分支對應的 ch 的 recv 或者 send goroutine queue 中,對同一個 chan 也可能將當前 goroutine 同時 push 到 recv哪怔、send goroutine queue 這兩個隊列中宣蔚。
不管是普通的 chan send、recv 操作认境,還是 select chan send胚委、recv 操作,因為 chan 操作阻塞的 goroutine 都是依靠其他 goroutine 對 chan 的 send叉信、recv 操作來喚醒的亩冬。前面我們已經(jīng)講過了 goroutine 被喚醒的時機,這里還要再細分一下硼身。
chan 的 send硅急、recv goroutine queue 中存儲的其實是一個結構體指針 * sudog枢冤,成員 gp * g 指向?qū)?goroutine,elem unsafe.Pointer 指向待讀寫的變量地址铜秆,c * hchan 指向 goroutine 阻塞在哪個 chan 上淹真,isSelect 為 true 表示 select chan send、recv连茧,反之表示 chan send核蘸、recv。g.selectDone 表示 select 操作是否處理完成啸驯,即是否有某個 case 分支已經(jīng)成立客扎。
2.1 chan 操作阻塞的 goroutine 喚醒時執(zhí)行邏輯
下面我們先描述下 chan 上某個 goroutine 被喚醒時的處理邏輯,假如現(xiàn)在有個 goroutine 因為 select chan 操作阻塞在了 ch1罚斗、ch2 上徙鱼,那么會創(chuàng)建對應的 sudog 對象,并將對應的指針 * sudog push 到各個 case 分支對應的 ch1针姿、ch2 上的 send袱吆、recv goroutine queue 中,等待其他協(xié)程執(zhí)行 (select) chan send距淫、recv 操作時將其喚醒: 1)源碼文件 chan.go绞绒,假如現(xiàn)在有另外一個 goroutine 對 ch1 進行了操作,然后對 ch1 的 goroutine 執(zhí)行 unshift 操作取出一個阻塞的 goroutine榕暇,在 unshift 時要執(zhí)行方法 **func (q *waitq) dequeue() sudog蓬衡,這個方法從 ch1 的等待隊列中返回一個阻塞的 goroutine。
func (q *waitq) dequeue() *sudog {
for {
sgp := q.first
if sgp == nil {
return nil
}
y := sgp.next
if y == nil {
q.first = nil
q.last = nil
} else {
y.prev = nil
q.first = y
sgp.next = nil // mark as removed (see dequeueSudog)
}
// if a goroutine was put on this queue because of a
// select, there is a small window between the goroutine
// being woken up by a different case and it grabbing the
// channel locks. Once it has the lock
// it removes itself from the queue, so we won't see it after that.
// We use a flag in the G struct to tell us when someone
// else has won the race to signal this goroutine but the goroutine
// hasn't removed itself from the queue yet.
if sgp.isSelect {
if !atomic.Cas(&sgp.g.selectDone, 0, 1) {
continue
}
}
return sgp
}
}
假如隊首元素就是之前阻塞的 goroutine彤枢,那么檢測到其 sgp.isSelect=true狰晚,就知道這是一個因為 select chan send、recv 阻塞的 goroutine缴啡,然后通過 CAS 操作將 sgp.g.selectDone 設為 true 標識當前 goroutine 的 select 操作已經(jīng)處理完成壁晒,之后就可以將該 goroutine 返回用于從 value buffer 讀或者向 value buffer 寫數(shù)據(jù)了,或者直接與喚醒它的 goroutine 交換數(shù)據(jù)盟猖,然后該阻塞的 goroutine 就可以恢復執(zhí)行了讨衣。
這里將 sgp.g.selectDone 設為 true,相當于傳達了該 sgp.g 已經(jīng)從剛才阻塞它的 select-case 塊中退出了式镐,對應的 select-case 塊可以作廢了反镇。有必要提提一下為什么要把這里的 sgp.g.selectDone 設為 true 呢?直接將該 goroutine 出隊不就完了嗎娘汞?不行歹茶!考慮以下對 chan 的操作 dequeue 是需要先拿到 chan 上的 lock 的,但是在嘗試 lock chan 之前有可能同時有多個 case 分支對應的 chan 準備就緒【颍看個示例代碼:
g1
go func() {
ch1 <- 1}()
// g2
go func() {
ch2 <- 2
}
select {
case <- ch1:
doSomething()
case <- ch2:
doSomething()
}
協(xié)程 g1 在 chan.chansend 方法中執(zhí)行了一般燎孟,準備 lock ch1,協(xié)程 g2 也執(zhí)行了一半尸昧,也準備 lock ch2; 協(xié)程 g1 成功 lock ch1 執(zhí)行 dequeue 操作揩页,協(xié)程 g2 頁成功 lock ch2 執(zhí)行 deq ueue 操作; 因為同一個 select-case 塊中只能有一個 case 分支允許激活烹俗,所以在協(xié)程 g 里面加了個成員 g.selectDone 來標識該協(xié)程對應的 select-case 是否已經(jīng)成功執(zhí)行結束(一個協(xié)程在某個時刻只可能有一個 select-case 塊在處理爆侣,要么阻塞沒執(zhí)行完,要么立即執(zhí)行完)幢妄,因此 dequeue 時要通過 CAS 操作來更新 g.selectDone 的值兔仰,更新成功者完成出隊操作激活 case 分支,CAS 失敗的則認為該 select-case 已經(jīng)有其他分支被激活蕉鸳,當前 case 分支作廢乎赴,select-case 結束。
這里的 CAS 操作也就是說的多個分支滿足條件時潮尝,golang 會隨機選擇一個分支執(zhí)行的道理榕吼。
2.2 select-case 塊 golang 是如何執(zhí)行處理的
源文件 select.go 中方法 *selectgo(sel hselect) ,實現(xiàn)了對 select-case 塊的處理邏輯衍锚,但是由于代碼篇幅較長友题,這里不再復制粘貼代碼,感興趣的可以自己查看戴质,這里只簡要描述下其執(zhí)行流程。
selectgo 邏輯處理簡述:
- 預處理部分 對各個 case 分支按照 ch 地址排序踢匣,保證后續(xù)按序加鎖告匠,避免產(chǎn)生死鎖問題;
- pass 1 部分處理各個 case 分支的判斷邏輯离唬,依次檢查各個 case 分支是否有立即可滿足 ch 讀寫操作的后专。如果當前分支有則立即執(zhí)行 ch 讀寫并回,select 處理結束输莺;沒有則繼續(xù)處理下一分支戚哎;如果所有分支均不滿足繼續(xù)執(zhí)行以下流程。
- pass 2 沒有一個 case 分支上 chan 操作立即可就緒嫂用,當前 goroutine 需要阻塞型凳,遍歷所有的 case 分支,分別構建 goroutine 對應的 sudog 并 push 到 case 分支對應 chan 的對應 goroutine queue 中嘱函。然后 gopark 掛起當前 goroutine甘畅,等待某個分支上 chan 操作完成來喚醒當前 goroutine。怎么被喚醒呢?前面提到了 chan.waitq.dequeue() 方法中通過 CAS 將 sudog.g.selectDone 設為 1 之后將該 sudog 返回并恢復執(zhí)行疏唾,其實也就是借助這個操作來喚醒蓄氧。
- pass 3 整個 select-case 塊已經(jīng)結束使命,之前阻塞的 goroutine 已被喚醒槐脏,其他 case 分支沒什么作用了喉童,需要廢棄掉,pass 3 部分會將該 goroutine 從之前阻塞它的 select-case 塊中各 case 分支對應的 chan recv顿天、send goroutine queue 中移除堂氯,通過方法 chan.waitq.dequeueSudog(sgp * sudog) 來從隊列中移除,隊列是雙向鏈表露氮,通過 sudog.prev 和 sudog.next 刪除 sudog 時間復雜度為 O(1)祖灰。
本文簡要描述了 golang 中 select-case 的實現(xiàn)邏輯,介紹了 goroutine 與 chan 操作之間的協(xié)作關系畔规。之前 ZMQ 作者 Martin Sustrik 仿著 golang 寫過一個面向 c 的庫局扶,libmill,實際實現(xiàn)思路差不多叁扫,感興趣的也可以翻翻看三妈,libmill 源碼分析。