您好,登錄后才能下訂單哦!
本篇內(nèi)容介紹了“simpread golang與select case的實現(xiàn)機制是什么”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!
當(dāng)一個 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 并讓其恢復(fù)執(zhí)行,讓其執(zhí)行把數(shù)據(jù)寫入 chan 的操作,實際上是恢復(fù)該發(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 將恢復(fù)執(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),什么時候恢復(fù)期執(zhí)行呢,要等到有一個 goroutine 嘗試向 chan 發(fā)送數(shù)據(jù)的時候了。這種場景下,chan 接收操作是 blocking 操作。
當(dāng)一個 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 的接收地址處,然后恢復(fù)該 recv goroutine 的運行,當(dāng)前發(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 中進(jìn)入阻塞狀態(tài)。等到有其他 goroutine 嘗試從 chan 接收數(shù)據(jù)的時候才能將其喚醒恢復(fù)執(zhí)行。這種情況下,chan send 操作是 blocking 操作。
當(dāng)一個 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 中存著。
一旦 chan 被關(guān)閉了,chan recv 操作就永遠(yuǎn)也不會阻塞,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 分支滿足條件然后將對應(yīng)的 goroutine 喚醒恢復(fù)執(zhí)行才可以繼續(xù)執(zhí)行,否則代碼就會阻塞在這里,即將當(dāng)前 goroutine push 到各個 case 分支對應(yīng)的 ch 的 recv 或者 send goroutine queue 中,對同一個 chan 也可能將當(dāng)前 goroutine 同時 push 到 recv、send goroutine queue 這兩個隊列中。
不管是普通的 chan send、recv 操作,還是 select chan send、recv 操作,因為 chan 操作阻塞的 goroutine 都是依靠其他 goroutine 對 chan 的 send、recv 操作來喚醒的。前面我們已經(jīng)講過了 goroutine 被喚醒的時機,這里還要再細(xì)分一下。
chan 的 send、recv goroutine queue 中存儲的其實是一個結(jié)構(gòu)體指針 * sudog,成員 gp * g 指向?qū)?yīng)的 goroutine,elem unsafe.Pointer 指向待讀寫的變量地址,c * hchan 指向 goroutine 阻塞在哪個 chan 上,isSelect 為 true 表示 select chan send、recv,反之表示 chan send、recv。g.selectDone 表示 select 操作是否處理完成,即是否有某個 case 分支已經(jīng)成立。
下面我們先描述下 chan 上某個 goroutine 被喚醒時的處理邏輯,假如現(xiàn)在有個 goroutine 因為 select chan 操作阻塞在了 ch2、ch3 上,那么會創(chuàng)建對應(yīng)的 sudog 對象,并將對應(yīng)的指針 * sudog push 到各個 case 分支對應(yīng)的 ch2、ch3 上的 send、recv goroutine queue 中,等待其他協(xié)程執(zhí)行 (select) chan send、recv 操作時將其喚醒: 1)源碼文件 chan.go,假如現(xiàn)在有另外一個 goroutine 對 ch2 進(jìn)行了操作,然后對 ch2 的 goroutine 執(zhí)行 unshift 操作取出一個阻塞的 goroutine,在 unshift 時要執(zhí)行方法 **func (q *waitq) dequeue() sudog,這個方法從 ch2 的等待隊列中返回一個阻塞的 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 設(shè)為 true 標(biāo)識當(dāng)前 goroutine 的 select 操作已經(jīng)處理完成,之后就可以將該 goroutine 返回用于從 value buffer 讀或者向 value buffer 寫數(shù)據(jù)了,或者直接與喚醒它的 goroutine 交換數(shù)據(jù),然后該阻塞的 goroutine 就可以恢復(fù)執(zhí)行了。
這里將 sgp.g.selectDone 設(shè)為 true,相當(dāng)于傳達(dá)了該 sgp.g 已經(jīng)從剛才阻塞它的 select-case 塊中退出了,對應(yīng)的 select-case 塊可以作廢了。有必要提提一下為什么要把這里的 sgp.g.selectDone 設(shè)為 true 呢?直接將該 goroutine 出隊不就完了嗎?不行!考慮以下對 chan 的操作 dequeue 是需要先拿到 chan 上的 lock 的,但是在嘗試 lock chan 之前有可能同時有多個 case 分支對應(yīng)的 chan 準(zhǔn)備就緒??磦€示例代碼:
g1 go func() { ch2 <- 1?}() // g2 go func() { ch3 <- 2 } select { case <- ch2: doSomething() case <- ch3: doSomething() }
協(xié)程 g1 在 chan.chansend 方法中執(zhí)行了一般,準(zhǔn)備 lock ch2,協(xié)程 g2 也執(zhí)行了一半,也準(zhǔn)備 lock ch3; 協(xié)程 g1 成功 lock ch2 執(zhí)行 dequeue 操作,協(xié)程 g2 頁成功 lock ch3 執(zhí)行 deq ueue 操作; 因為同一個 select-case 塊中只能有一個 case 分支允許激活,所以在協(xié)程 g 里面加了個成員 g.selectDone 來標(biāo)識該協(xié)程對應(yīng)的 select-case 是否已經(jīng)成功執(zhí)行結(jié)束(一個協(xié)程在某個時刻只可能有一個 select-case 塊在處理,要么阻塞沒執(zhí)行完,要么立即執(zhí)行完),因此 dequeue 時要通過 CAS 操作來更新 g.selectDone 的值,更新成功者完成出隊操作激活 case 分支,CAS 失敗的則認(rèn)為該 select-case 已經(jīng)有其他分支被激活,當(dāng)前 case 分支作廢,select-case 結(jié)束。
這里的 CAS 操作也就是說的多個分支滿足條件時,golang 會隨機選擇一個分支執(zhí)行的道理。
源文件 select.go 中方法 *selectgo(sel hselect) ,實現(xiàn)了對 select-case 塊的處理邏輯,但是由于代碼篇幅較長,這里不再復(fù)制粘貼代碼,感興趣的可以自己查看,這里只簡要描述下其執(zhí)行流程。
selectgo 邏輯處理簡述:
預(yù)處理部分 對各個 case 分支按照 ch 地址排序,保證后續(xù)按序加鎖,避免產(chǎn)生死鎖問題;
pass 1 部分處理各個 case 分支的判斷邏輯,依次檢查各個 case 分支是否有立即可滿足 ch 讀寫操作的。如果當(dāng)前分支有則立即執(zhí)行 ch 讀寫并回,select 處理結(jié)束;沒有則繼續(xù)處理下一分支;如果所有分支均不滿足繼續(xù)執(zhí)行以下流程。
pass 2 沒有一個 case 分支上 chan 操作立即可就緒,當(dāng)前 goroutine 需要阻塞,遍歷所有的 case 分支,分別構(gòu)建 goroutine 對應(yīng)的 sudog 并 push 到 case 分支對應(yīng) chan 的對應(yīng) goroutine queue 中。然后 gopark 掛起當(dāng)前 goroutine,等待某個分支上 chan 操作完成來喚醒當(dāng)前 goroutine。怎么被喚醒呢?前面提到了 chan.waitq.dequeue() 方法中通過 CAS 將 sudog.g.selectDone 設(shè)為 1 之后將該 sudog 返回并恢復(fù)執(zhí)行,其實也就是借助這個操作來喚醒。
pass 3 整個 select-case 塊已經(jīng)結(jié)束使命,之前阻塞的 goroutine 已被喚醒,其他 case 分支沒什么作用了,需要廢棄掉,pass 3 部分會將該 goroutine 從之前阻塞它的 select-case 塊中各 case 分支對應(yīng)的 chan recv、send goroutine queue 中移除,通過方法 chan.waitq.dequeueSudog(sgp * sudog) 來從隊列中移除,隊列是雙向鏈表,通過 sudog.prev 和 sudog.next 刪除 sudog 時間復(fù)雜度為 O(1)。
“simpread golang與select case的實現(xiàn)機制是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。