Go Select 詳解

[TOC]

導(dǎo)讀

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

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

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

golang中的select語(yǔ)句格式如下

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

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

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

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

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

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

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

示例

  1. select語(yǔ)句只能用于信道的讀寫操作
 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")
    }

語(yǔ)句會(huì)報(bào)錯(cuò)

select case must be receive, send or assign recv
從錯(cuò)誤信息里我們證實(shí)了第一點(diǎn)。

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)
    }
}

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

場(chǎng)景

select這個(gè)特性到底有什么用呢,下面我們來(lái)介紹一些使用select的場(chǎng)景

競(jìng)爭(zhēng)選舉
    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)
    ...
    }

這個(gè)是最常見的使用場(chǎng)景,多個(gè)通道,有一個(gè)滿足條件可以讀取,就可以“競(jìng)選成功”

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

因?yàn)閟elect是阻塞的,我們有時(shí)候就需要搭配超時(shí)處理來(lái)處理這種情況,超過(guò)某一個(gè)時(shí)間就要進(jìn)行處理,保證程序不阻塞。

判斷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("資源已滿,請(qǐng)稍后再試")
            time.Sleep(time.Second) 
        } 
    }
}

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

阻塞main函數(shù)

有時(shí)候我們會(huì)讓main函數(shù)阻塞不退出,如http服務(wù),我們會(huì)使用空的select{}來(lái)阻塞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ù)就永遠(yuǎn)阻塞住了,這里要注意上面一定要有一直活動(dòng)的goroutine,否則會(huì)報(bào)deadlock。大家還可以把select{}換成for{}試一下,打開系統(tǒng)管理器看下CPU的占用變化。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,778評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,218評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,436評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,795評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,993評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,229評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,687評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,990評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容