本文由 簡悅 SimpRead 轉碼, 原文地址 https://hitzhangjie.github.io...
在介紹 select-case 實現機制以前,最好先了解下 chan 操做規則,明白 goroutine 什麼時候阻塞,又在什麼時機被喚醒,這對後續理解 select-case 實現有幫助。因此接下來先介紹 chan 操做規則,而後再介紹 select-case 的實現。html
當一個 goroutine 要從一個 non-nil & non-closed chan 上接收數據時,goroutine 首先會去獲取 chan 上的鎖,而後執行以下操做直到某個條件被知足:git
1)若是 chan 上的 value buffer 不空,這也意味着 chan 上的 recv goroutine queue 也必定是空的,該接收 goroutine 將從 value buffer 中 unshift 出一個 value。這個時候,若是 send goroutine 隊列不空的狀況下,由於剛纔 value buffer 中空出了一個位置,有位置可寫,因此這個時候會從 send goroutine queue 中 unshift 出一個發送 goroutine 並讓其恢復執行,讓其執行把數據寫入 chan 的操做,其實是恢復該發送該 goroutine 執行,並把該發送 goroutine 要發送的數據 push 到 value buffer 中。而後呢,該接收 goroutine 也拿到了數據了,就繼續執行。這種情景,channel 的接收操做稱爲 non-blocking 操做。github
2)另外一種狀況,若是 value buffer 是空的,可是 send goroutine queue 不空,這種狀況下,該 chan 必定是 unbufferred chan,否則 value buffer 確定有數據嘛,這個時候接收 goroutine 將從 send goroutine queue 中 unshift 出一個發送 goroutine,並將該發送 goroutine 要發送的數據接收過來(兩個 goroutine 一個有發送數據地址,一個有接收數據地址,拷貝過來就 ok),而後這個取出的發送 goroutine 將恢復執行,這個接收 goroutine 也能夠繼續執行。這種狀況下,chan 接收操做也是 non-blocking 操做。golang
3)另外一種狀況,若是 value buffer 和 send goroutine queue 都是空的,沒有數據可接收,將把該接收 goroutine push 到 chan 的 recv goroutine queue,該接收 goroutine 將轉入 blocking 狀態,何時恢復期執行呢,要等到有一個 goroutine 嘗試向 chan 發送數據的時候了。這種場景下,chan 接收操做是 blocking 操做。源碼分析
當一個 goroutine 常識向一個 non-nil & non-closed chan 發送數據的時候,該 goroutine 將先嚐試獲取 chan 上的鎖,而後執行以下操做直到知足其中一種狀況。this
1)若是 chan 的 recv goroutine queue 不空,這種狀況下,value buffer 必定是空的。發送 goroutine 將從 recv goroutine queue 中 unshift 出一個 recv goroutine,而後直接將本身要發送的數據拷貝到該 recv goroutine 的接收地址處,而後恢復該 recv goroutine 的運行,當前發送 goroutine 也繼續執行。這種狀況下,chan send 操做是 non-blocking 操做。atom
2)若是 chan 的 recv goroutine queue 是空的,而且 value buffer 不滿,這種狀況下,send goroutine queue 必定是空的,由於 value buffer 不滿發送 goroutine 能夠發送完成不可能會阻塞。該發送 goroutine 將要發送的數據 push 到 value buffer 中而後繼續執行。這種狀況下,chan send 操做是 non-blocking 操做。spa
3)若是 chan 的 recv goroutine queue 是空的,而且 value buffer 是滿的,發送 goroutine 將被 push 到 send goroutine queue 中進入阻塞狀態。等到有其餘 goroutine 嘗試從 chan 接收數據的時候才能將其喚醒恢復執行。這種狀況下,chan send 操做是 blocking 操做。指針
當一個 goroutine 嘗試 close 一個 non-nil & non-closed chan 的時候,close 操做將依次執行以下操做。code
1)若是 chan 的 recv goroutine queue 不空,這種狀況下 value buffer 必定是空的,由於若是 value buffer 若是不空,必定會繼續 unshift recv goroutine queue 中的 goroutine 接收數據,直到 value buffer 爲空(這裏能夠看下 chan send 操做,chan send 寫入數據以前,必定會從 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 以前發送到 chan 的數據仍然在 chan 的 value buffer 中存着。
一旦 chan 被關閉了,chan recv 操做就永遠也不會阻塞,chan 的 value buffer 中在 close 以前寫入的數據仍然存在。一旦 value buffer 中 close 以前寫入的數據都被取出以後,後續的接收操做將會返回 val=0 和 sentBeforeClosed=true。
理解這裏的 goroutine 的 blocking、non-blocking 操做對於理解針對 chan 的 select-case 操做是頗有幫助的。下面介紹 select-case 實現機制。
select-case 中假如沒有 default 分支的話,必定要等到某個 case 分支知足條件而後將對應的 goroutine 喚醒恢復執行才能夠繼續執行,不然代碼就會阻塞在這裏,即將當前 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 操做來喚醒的。前面咱們已經講過了 goroutine 被喚醒的時機,這裏還要再細分一下。
chan 的 send、recv goroutine queue 中存儲的實際上是一個結構體指針 sudog,成員 gp g 指向對應的 goroutine,elem unsafe.Pointer 指向待讀寫的變量地址,c * hchan 指向 goroutine 阻塞在哪一個 chan 上,isSelect 爲 true 表示 select chan send、recv,反之表示 chan send、recv。g.selectDone 表示 select 操做是否處理完成,便是否有某個 case 分支已經成立。
下面咱們先描述下 chan 上某個 goroutine 被喚醒時的處理邏輯,假如如今有個 goroutine 由於 select chan 操做阻塞在了 ch一、ch2 上,那麼會建立對應的 sudog 對象,並將對應的指針 sudog push 到各個 case 分支對應的 ch一、ch2 上的 send、recv goroutine queue 中,等待其餘協程執行 (select) chan send、recv 操做時將其喚醒: 1)源碼文件 chan.go,假如如今有另一個 goroutine 對 ch1 進行了操做,而後對 ch1 的 goroutine 執行 unshift 操做取出一個阻塞的 goroutine,在 unshift 時要執行方法 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 操做已經處理完成,以後就能夠將該 goroutine 返回用於從 value buffer 讀或者向 value buffer 寫數據了,或者直接與喚醒它的 goroutine 交換數據,而後該阻塞的 goroutine 就能夠恢復執行了。
這裏將 sgp.g.selectDone 設爲 true,至關於傳達了該 sgp.g 已經從剛纔阻塞它的 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() }
協程 g1 在 chan.chansend 方法中執行了通常,準備 lock ch1,協程 g2 也執行了一半,也準備 lock ch2; 協程 g1 成功 lock ch1 執行 dequeue 操做,協程 g2 頁成功 lock ch2 執行 deq ueue 操做; 由於同一個 select-case 塊中只能有一個 case 分支容許激活,因此在協程 g 裏面加了個成員 g.selectDone 來標識該協程對應的 select-case 是否已經成功執行結束(一個協程在某個時刻只可能有一個 select-case 塊在處理,要麼阻塞沒執行完,要麼當即執行完),所以 dequeue 時要經過 CAS 操做來更新 g.selectDone 的值,更新成功者完成出隊操做激活 case 分支,CAS 失敗的則認爲該 select-case 已經有其餘分支被激活,當前 case 分支做廢,select-case 結束。
這裏的 CAS 操做也就是說的多個分支知足條件時,golang 會隨機選擇一個分支執行的道理。
源文件 select.go 中方法 selectgo(sel *hselect) ,實現了對 select-case 塊的處理邏輯,可是因爲代碼篇幅較長,這裏再也不復制粘貼代碼,感興趣的能夠本身查看,這裏只簡要描述下其執行流程。
selectgo 邏輯處理簡述:
本文簡要描述了 golang 中 select-case 的實現邏輯,介紹了 goroutine 與 chan 操做之間的協做關係。以前 ZMQ 做者 Martin Sustrik 仿着 golang 寫過一個面向 c 的庫,libmill,實際實現思路差很少,感興趣的也能夠翻翻看,libmill 源碼分析。