[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)題
示例
- 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的占用變化。