溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

simpread golang與select case的實現(xiàn)機制是什么

發(fā)布時間:2021-11-15 10:58:29 來源:億速云 閱讀:115 作者:iii 欄目:大數(shù)據(jù)

本篇內(nèi)容介紹了“simpread golang與select case的實現(xiàn)機制是什么”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!


1.1 chan 操作規(guī)則 1

當(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 操作。

1.2 chan 操作規(guī)則 2

當(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 操作。

1.3 chan 操作規(guī)則 3

當(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 中存著。

1.4 chan 操作規(guī)則 4

一旦 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)成立。

2.1 chan 操作阻塞的 goroutine 喚醒時執(zhí)行邏輯

下面我們先描述下 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í)行的道理。

2.2 select-case 塊 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ì)量的實用文章!

向AI問一下細(xì)節(jié)

免責(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)容。

AI