Go select的使用和實現原理


 一、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數組的長度

函數返回值:

  1. int: 選中case的編號,這個case編號跟代碼一致
  2. 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/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM