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