一、select簡介
1.Go的select語句是一種僅能用於channl發送和接收消息的專用語句,此語句運行期間是阻塞的;當select中沒有case語句的時候,會阻塞當前groutine。
2.select是Golang在語言層面提供的I/O多路復用的機制,其專門用來檢測多個channel是否准備完畢:可讀或可寫。
3.select語句中除default外,每個case操作一個channel,要么讀要么寫
4.select語句中除default外,各case執行順序是隨機的
5.select語句中如果沒有default語句,則會阻塞等待任一case
6.select語句中讀操作要判斷是否成功讀取,關閉的channel也可以讀取
二、用法例子
實例一:select語句中除default外,各case執行順序是隨機的
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.
實例二:select語句中如果沒有default語句,則會阻塞等待任一case
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。所以上述程序會一直阻塞。
實例三:select語句中讀操作要判斷是否成功讀取,關閉的channel也可以讀取
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.") }
實例四:
package main func main() { select { } }
上面程序中只有一個空的select語句。
參考答案:對於空的select語句,程序會被阻塞,准確的說是當前協程被阻塞,同時Golang自帶死鎖檢測機制,當發現當前協程再也沒有機會被喚醒時,則會panic。所以上述程序會panic。
三、select實現原理
Golang實現select時,定義了一個數據結構表示每個case語句(含defaut,default實際上是一種特殊的case),select執行過程可以類比成一個函數,函數輸入case數組,輸出選中的case,然后程序流程轉到選中的case塊。
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的數據存放地址;
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。
參考文獻:
https://my.oschina.net/renhc/blog/2253937/nhttps://blog.csdn.net/xdrbt/article/details/80287959
https://blog.csdn.net/wangxindong11/article/details/78591308
https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-select/