Go Select 詳解

[TOC]

導(dǎo)讀

select是一種go可以處理多個通道之間的機制,看起來和switch語句很相似,但是select其實和IO機制中的select一樣,多路復(fù)用通道,隨機選取一個進行執(zhí)行,如果說通道(channel)實現(xiàn)了多個goroutine之前的同步或者通信,那么select則實現(xiàn)了多個通道(channel)的同步或者通信,并且select具有阻塞的特性。

select 是 Go 中的一個控制結(jié)構(gòu),類似于用于通信的 switch 語句。每個 case 必須是一個通信操作,要么是發(fā)送要么是接收。

select 隨機執(zhí)行一個可運行的 case。如果沒有 case 可運行,它將阻塞,直到有 case 可運行。一個默認的子句應(yīng)該總是可運行的。

golang中的select語句格式如下

select {
    case <-ch1:
        // 如果從 ch1 信道成功接收數(shù)據(jù),則執(zhí)行該分支代碼
    case ch2 <- 1:
        // 如果成功向 ch2 信道成功發(fā)送數(shù)據(jù),則執(zhí)行該分支代碼
    default:
        // 如果上面都沒有成功,則進入 default 分支處理流程
}

可以看到select的語法結(jié)構(gòu)有點類似于switch,但又有些不同。

select里的case后面并不帶判斷條件,而是一個信道的操作,不同于switch里的case,對于從其它語言轉(zhuǎn)過來的開發(fā)者來說有些需要特別注意的地方。

golang 的 select 就是監(jiān)聽 IO 操作,當(dāng) IO 操作發(fā)生時,觸發(fā)相應(yīng)的動作每個case語句里必須是一個IO操作,確切的說,應(yīng)該是一個面向channel的IO操作。

注:Go 語言的 select 語句借鑒自 Unix 的 select() 函數(shù),在 Unix 中,可以通過調(diào)用 select() 函數(shù)來監(jiān)控一系列的文件句柄,一旦其中一個文件句柄發(fā)生了 IO 動作,該 select() 調(diào)用就會被返回(C 語言中就是這么做的),后來該機制也被用于實現(xiàn)高并發(fā)的 Socket 服務(wù)器程序。Go 語言直接在語言級別支持 select關(guān)鍵字,用于處理并發(fā)編程中通道之間異步 IO 通信問題。

注意:如果 ch1 或者 ch2 信道都阻塞的話,就會立即進入 default 分支,并不會阻塞。但是如果沒有 default 語句,則會阻塞直到某個信道操作成功為止。

  • select語句只能用于信道的讀寫操作
  • select中的case條件(非阻塞)是并發(fā)執(zhí)行的,select會選擇先操作成功的那個case條件去執(zhí)行,如果多個同時返回,則隨機選擇一個執(zhí)行,此時將無法保證執(zhí)行順序。對于阻塞的case語句會直到其中有信道可以操作,如果有多個信道可操作,會隨機選擇其中一個 case 執(zhí)行
  • 對于case條件語句中,如果存在信道值為nil的讀寫操作,則該分支將被忽略,可以理解為從select語句中刪除了這個case語句
  • 如果有超時條件語句,判斷邏輯為如果在這個時間段內(nèi)一直沒有滿足條件的case,則執(zhí)行這個超時case。如果此段時間內(nèi)出現(xiàn)了可操作的case,則直接執(zhí)行這個case。一般用超時語句代替了default語句
  • 對于空的select{},會引起死鎖
  • 對于for中的select{}, 也有可能會引起cpu占用過高的問題

示例

  1. select語句只能用于信道的讀寫操作
 select {
    case 3 == 3:
        fmt.Println("equal")
    case v := <-ch:
        fmt.Print(v)
    case b := <-ch2:
        fmt.Print(b)
    case ch3 <- 10:
        fmt.Print("write")
    default:
        fmt.Println("none")
    }

語句會報錯

select case must be receive, send or assign recv
從錯誤信息里我們證實了第一點。

package main
import (
    "fmt" 
    "time"
)

func main()  {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go func1 ()  {
        time.Sleep(time.Second)
        ch1 <- 1
    }()

    go func2 ()  {
        ch2 <- 3
    }()

    select {
    case i := <-ch1:
        fmt.Printf("從ch1讀取了數(shù)據(jù)%d", i)
    case j := <-ch2:
        fmt.Printf("從ch2讀取了數(shù)據(jù)%d", j)
    }
}

上面這段代碼很簡單,我們創(chuàng)建了兩個無緩沖的channel,通過兩個goroutine向ch1,ch2兩個通道發(fā)送數(shù)據(jù),通過select隨機讀取ch1,ch2的返回值,但是由于func1有sleep,所以這個例子我們總是從ch2讀到結(jié)果,打印從ch2讀取了數(shù)據(jù)3

場景

select這個特性到底有什么用呢,下面我們來介紹一些使用select的場景

競爭選舉
    select {
    case i := <-ch1:
        fmt.Printf("從ch1讀取了數(shù)據(jù)%d", i)
    case j := <-ch2:
        fmt.Printf("從ch2讀取了數(shù)據(jù)%d", j)
    case m := <- ch3
        fmt.Printf("從ch3讀取了數(shù)據(jù)%d", m)
    ...
    }

這個是最常見的使用場景,多個通道,有一個滿足條件可以讀取,就可以“競選成功”

超時處理(保證不阻塞)
select {
    case str := <- ch1
        fmt.Println("receive str", str)
    case <- time.After(time.Second * 5): 
        fmt.Println("timeout!!")
}

因為select是阻塞的,我們有時候就需要搭配超時處理來處理這種情況,超過某一個時間就要進行處理,保證程序不阻塞。

判斷buffered channel是否阻塞
package main
import (
    "fmt" 
    "time"
)

func main()  {
    bufChan := make(chan int, 5)
    
    go func ()  {
        time.Sleep(time.Second)
        for {
            <-bufChan
            time.Sleep(5*time.Second)
        }
    }() 
    

    for {
        select {    
        case bufChan <- 1:  
            fmt.Println("add success")
            time.Sleep(time.Second)  
        default:        
            fmt.Println("資源已滿,請稍后再試")
            time.Sleep(time.Second) 
        } 
    }
}

這個例子很經(jīng)典,比如我們有一個有限的資源(這里用buffer channel實現(xiàn)),我們每一秒向bufChan傳送數(shù)據(jù),由于生產(chǎn)者的生產(chǎn)速度大于消費者的消費速度,故會觸發(fā)default語句,這個就很像我們web端來顯示并發(fā)過高的提示了,小伙伴們可以嘗試刪除go func中的time.Sleep(5*time.Second),看看是否還會觸發(fā)default語句

阻塞main函數(shù)

有時候我們會讓main函數(shù)阻塞不退出,如http服務(wù),我們會使用空的select{}來阻塞main goroutine

package main
import (
    "fmt"
    "time"
)

func main()  {
    bufChan := make(chan int)
    
    go func() {
        for{
            bufChan <-1
            time.Sleep(time.Second)
        }
    }()


    go func() {
        for{
            fmt.Println(<-bufChan)
        }
    }()
     
    select{}
}

如上所示,這樣主函數(shù)就永遠阻塞住了,這里要注意上面一定要有一直活動的goroutine,否則會報deadlock。大家還可以把select{}換成for{}試一下,打開系統(tǒng)管理器看下CPU的占用變化。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容