1. 前言
select是Golang在語言層面提供的多路IO複用的機制,其可以檢測多個channel是否ready(即是否可讀或可寫),使用起來非常方便。
本章試圖根據源碼總結其實現原理,從而發現一些使用誤區或解釋一些不太常見的現象。
2. 熱身環節
我們先看幾個題目,用於測試對select的瞭解程度,每個題目代表一個知識點,本章後面的部分會進行略爲詳細的介紹。
2.1 題目1
下面的程序輸出是什麼?
package main
import (
"fmt"
"time"
)
func main() {
chan1 := make(chan int)
chan2 := make(chan int)
go func() {
chan1 <- 1
time.Sleep(5 * time.Second)
}()
go func() {
chan2 <- 1
time.Sleep(5 * time.Second)
}()
select {
case <-chan1:
fmt.Println("chan1 ready.")
case <-chan2:
fmt.Println("chan2 ready.")
default:
fmt.Println("default")
}
fmt.Println("main exit.")
}
程序中聲明兩個channel,分別爲chan1和chan2,依次啓動兩個協程,分別向兩個channel中寫入一個數據就進入睡眠。select語句兩個case分別檢測chan1和chan2是否可讀,如果都不可讀則執行default語句。
參考答案:
select中各個case執行順序是隨機的,如果某個case中的channel已經ready,則執行相應的語句並退出select流程,如果所有case中的channel都未ready,則執行default中的語句然後退出select流程。另外,由於啓動的協程和select語句並不能保證執行順序,所以也有可能select執行時協程還未向channel中寫入數據,所以select直接執行default語句並退出。所以,以下三種輸出都有可能:
可能的輸出一:
chan1 ready.
main exit.
可能的輸出二:
chan2 ready.
main exit.
可能的輸出三:
default
main exit.
2.2 題目2
下面的程序執行到select時會發生什麼?
package main
import (
"fmt"
"time"
)
func main() {
chan1 := make(chan int)
chan2 := make(chan int)
writeFlag := false
go func() {
for {
if writeFlag {
chan1 <- 1
}
time.Sleep(time.Second)
}
}()
go func() {
for {
if writeFlag {
chan2 <- 1
}
time.Sleep(time.Second)
}
}()
select {
case <-chan1:
fmt.Println("chan1 ready.")
case <-chan2:
fmt.Println("chan2 ready.")
}
fmt.Println("main exit.")
}
程序中聲明兩個channel,分別爲chan1和chan2,依次啓動兩個協程,協程會判斷一個bool類型的變量writeFlag來決定是否要向channel中寫入數據,由於writeFlag永遠爲false,所以實際上協程什麼也沒做。select語句兩個case分別檢測chan1和chan2是否可讀,這個select語句不包含default語句。
參考答案:select會按照隨機的順序檢測各case語句中channel是否ready,如果某個case中的channel已經ready則執行相應的case語句然後退出select流程,如果所有的channel都未ready且沒有default的話,則會阻塞等待各個channel。所以上述程序會一直阻塞。
2.3 題目3
下面程序有什麼問題?
package main
import (
"fmt"
)
func main() {
chan1 := make(chan int)
chan2 := make(chan int)
go func() {
close(chan1)
}()
go func() {
close(chan2)
}()
select {
case <-chan1:
fmt.Println("chan1 ready.")
case <-chan2:
fmt.Println("chan2 ready.")
}
fmt.Println("main exit.")
}
程序中聲明兩個channel,分別爲chan1和chan2,依次啓動兩個協程,協程分別關閉兩個channel。select語句兩個case分別檢測chan1和chan2是否可讀,這個select語句不包含default語句。
參考答案:select會按照隨機的順序檢測各case語句中channel是否ready,考慮到已關閉的channel也是可讀的,所以上述程序中select不會阻塞,具體執行哪個case語句具是隨機的。
2.4 題目4
下面程序會發生什麼?
package main
func main() {
select {
}
}
上面程序中只有一個空的select語句。
參考答案:對於空的select語句,程序會被阻塞,準確的說是當前協程被阻塞,同時Golang自帶死鎖檢測機制,當發現當前協程再也沒有機會被喚醒時,則會panic。所以上述程序會panic。
3. 實現原理
Golang實現select時,定義了一個數據結構表示每個case語句(含defaut,default實際上是一種特殊的case),select執行過程可以類比成一個函數,函數輸入case數組,輸出選中的case,然後程序流程轉到選中的case塊。
3.1 case數據結構
源碼包src/runtime/select.go:scase
定義了表示case語句的數據結構:
type scase struct {
c *hchan // chan
kind uint16
elem unsafe.Pointer // data element
}
scase.c爲當前case語句所操作的channel指針,這也說明了一個case語句只能操作一個channel。
scase.kind表示該case的類型,分爲讀channel、寫channel和default,三種類型分別由常量定義:
- caseRecv:case語句中嘗試讀取scase.c中的數據;
- caseSend:case語句中嘗試向scase.c中寫入數據;
- caseDefault: default語句
scase.elem表示緩衝區地址,跟據scase.kind不同,有不同的用途:
- scase.kind == caseRecv : scase.elem表示讀出channel的數據存放地址;
- scase.kind == caseSend : scase.elem表示將要寫入channel的數據存放地址;
3.2 select實現邏輯
源碼包src/runtime/select.go:selectgo()
定義了select選擇case的函數:
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)
函數參數:
- cas0爲scase數組的首地址,selectgo()就是從這些scase中找出一個返回。
- order0爲一個兩倍cas0數組長度的buffer,保存scase隨機序列pollorder和scase中channel地址序列lockorder
- pollorder:每次selectgo執行都會把scase序列打亂,以達到隨機檢測case的目的。
- lockorder:所有case語句中channel序列,以達到去重防止對channel加鎖時重複加鎖的目的。
- ncases表示scase數組的長度
函數返回值:
- int: 選中case的編號,這個case編號跟代碼一致
- bool: 是否成功從channle中讀取了數據,如果選中的case是從channel中讀數據,則該返回值表示是否讀取成功。
selectgo實現僞代碼如下:
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
//1. 鎖定scase語句中所有的channel
//2. 按照隨機順序檢測scase中的channel是否ready
// 2.1 如果case可讀,則讀取channel中數據,解鎖所有的channel,然後返回(case index, true)
// 2.2 如果case可寫,則將數據寫入channel,解鎖所有的channel,然後返回(case index, false)
// 2.3 所有case都未ready,則解鎖所有的channel,然後返回(default index, false)
//3. 所有case都未ready,且沒有default語句
// 3.1 將當前協程加入到所有channel的等待隊列
// 3.2 當將協程轉入阻塞,等待被喚醒
//4. 喚醒後返回channel對應的case index
// 4.1 如果是讀操作,解鎖所有的channel,然後返回(case index, true)
// 4.2 如果是寫操作,解鎖所有的channel,然後返回(case index, false)
}
特別說明:對於讀channel的case來說,如case elem, ok := <-chan1:
, 如果channel有可能被其他協程關閉的情況下,一定要檢測讀取是否成功,因爲close的channel也有可能返回,此時ok == false。
4. 總結
- select語句中除default外,每個case操作一個channel,要麼讀要麼寫
- select語句中除default外,各case執行順序是隨機的
- select語句中如果沒有default語句,則會阻塞等待任一case
- select語句中讀操作要判斷是否成功讀取,關閉的channel也可以讀取