通過select語句可以監(jiān)聽channel上的數(shù)據(jù)流動
Golang的select
語句類似于UNIX的select()
函數(shù)的輪詢機制吃警,在UNIX中可通過調(diào)用select()
函數(shù)來監(jiān)控一系列的文件句柄(文件描述符姓言,數(shù)量有限)境蜕,一旦某個文件句柄發(fā)生了I/O動作select()
調(diào)用就會被返回。該機制也被用于高并發(fā)的Socket服務器程序中则吟。
select
來源于網(wǎng)絡I/O模型中的select
伙狐,本質上I/O多路復用技術抒钱,只不過Golang中的select
基于的并非網(wǎng)絡而是channel
。
select {
case communication clause:
statement(s)
case communication clause:
statement(s)
default:
statement(s)
}
select
語句是Golang中的控制結構鼎天,類似用于通信的switch
語句舀奶,也被稱為channel
開關。select
語句會等待channel
準備就緒(收發(fā)操作)斋射,以便在不同的case
下執(zhí)行育勺。由select
開始的一個新的選擇塊,每個選擇條件由case
語句來描述罗岖。與switch
語句相比涧至,select
有較多限制,最大的限制在于每個case
語句必須是一個I/O操作桑包。
select{
case v1 := <-ch1:
fmt.Printf("[CH1] received: %v\n", v1)
case v2,ok := <-ch2:
if ok{
fmt.Printf("[CH2] received: %v\n", v2)
}else{
fmt.Printf("[CH2] closed\n")
}
case ch3 <-msg:
fmt.Printf("[CH3] sent: %v\n", msg)
default:
fmt.Printf("NO Communicating\n")
}
例如:上例select
語句擁有四個case
子句化借,前兩個是channel
的receive
接收操作,第三個是channel
的send
發(fā)送操作捡多,最后一個default
默認操作蓖康。當代碼執(zhí)行到select
語句時,case
子句會按源代碼的順序進行評估垒手,且只評估一次蒜焊。評估結果會出現(xiàn)下面幾種情況:
- 除
default
外,若只有一個case
評估通過則執(zhí)行此case
中的語句科贬。 - 除
default
外泳梆,若用多個case
評估通過則隨機挑選一個執(zhí)行。 - 除
default
外榜掌,所有的case
評估都不通過优妙,則執(zhí)行default
。 - 若沒有
default
則select
代碼塊發(fā)生阻塞憎账,直到有一個case
通過評估套硼,否則一直阻塞。 - 若
case
中receive
接收的是nil
則也會發(fā)生阻塞
監(jiān)聽
Golang的select
用來監(jiān)聽和channel
有關的I/O操作胞皱,可監(jiān)聽進入channel
時的數(shù)據(jù)邪意,也可以是用channel
發(fā)送值時九妈。當I/O操作發(fā)生時會觸發(fā)相應地動作,因此每個case
都必須是一個I/O操作雾鬼,確切的說應該是一個面向channel
的I/O操作萌朱。
例如:定時器
ticker1 := time.NewTicker(time.Second * 1)
ticker2 := time.NewTicker(time.Second * 3)
for{
select{
case <-ticker1.C:
fmt.Printf("[1] TICK\n")
case <-ticker2.C:
fmt.Printf("[2] TICK\n")
}
}
在執(zhí)行select
語句時,運行時系統(tǒng)會自上而下地判斷每個case
中的發(fā)送或接收操作是否可以被立即執(zhí)行策菜,所謂立即執(zhí)行即當前goroutine
不會因此操作而被阻塞晶疼。
select
語句只能用于channel
的讀寫操作
例如:使用select
來檢測channel
是否已滿
ch := make(chan int, 1)
ch<-1
select{
case ch<-2:
fmt.Printf("send to channel\n")
default:
fmt.Printf("channel is full\n")
}
特性
- 每個
case
都必須是一個channel
- 所有
channel
表達式都會被求值 - 所有被發(fā)送的表達式都會被求值
- 若任意某個
channel
可執(zhí)行就執(zhí)行,其它被忽略又憨。
例如:獲取斐波拉茲數(shù)列
func fib(ch, quit chan int){
x,y := 0,1
for{
select{
case ch <- x:
x,y = y, x+y
case <-quit:
fmt.Printf("QUIT\n")
return
}
}
}
func main(){
ch := make(chan int)
quit := make(chan int)
go func(){
for i:=0; i<10; i++{
fmt.Println(<-ch)
}
quit<-0
}()
fib(ch, quit)
}
case
-
select
中每個case
必須是一個channel
操作冒晰,要么是發(fā)送要么是接收。
select
執(zhí)行過程中必須命中某一case
分支竟块,若在遍歷所有case
后都沒有命中壶运,則會進入default
分支。若沒有default
分支則select
發(fā)生阻塞浪秘,直到某個case
可以命中蒋情。若一直都沒有命中,則select
拋出deadlock
死鎖錯誤耸携。
- 循環(huán)中每次
select
都會對所有channel
表達式求值
例如:通過time.After
實現(xiàn)定時器棵癣,定時任務可通過done channel
停止。
done := make(chan bool, 1)
close(done)
for{
select{
case <-time.After(time.Second):
fmt.Printf("Time after\n")
case <-done:
//讀取零值 false
fmt.Printf("Read done\n")
}
}
- 若多個
case
滿足讀寫條件夺衍,select
會隨機選擇一個case
來執(zhí)行狈谊。
select
會隨機執(zhí)行一個可運行的case
,若沒有case
可以運行則阻塞沟沙,直到有case
可以運行河劝。
select
可用于多個channel
進行讀寫操作時僅需一次只處理一個的情況。
ch := make(chan int, 1024)
go func(ch chan int){
for{
v := <-ch
fmt.Printf("value = %v\n", v)
}
}(ch)
ticker := time.NewTicker(time.Second * 1)
for i:=0; i<5; i++{
select{
case ch<-i:
case <-ticker.C:
fmt.Printf("%d: Ticker\n", i)
}
time.Sleep(time.Microsecond * 500)
}
close(ch)
ticker.Stop()
若ticker.C
和ch
同時滿足讀寫條件時矛紫,select
會隨機地選擇一個來執(zhí)行赎瞎,導致看起來一些數(shù)據(jù)丟了。
- 對于
case
條件語句中若存在channel
值為nil
的讀寫操作颊咬,則該分支會被忽略务甥。
var ch chan int
go func(ch chan int){
ch <- 100
}(ch)
select {
case <-ch:
fmt.Printf("Channel recieved\n")
}
發(fā)生錯誤:fatal error: all goroutines are asleep - deadlock!
default
select
語句會被阻塞,直到其中一個case
被執(zhí)行喳篇。若select
中沒有任何case
敞临,它將永遠阻塞,從而導致死鎖麸澜。
例如:空select{}
引發(fā)死鎖
func main(){
select {
}
}
對于空的select
語句挺尿,程序會被阻塞,準確來說是當前goroutine
會被阻塞。Golang自帶死鎖檢測機制票髓,發(fā)現(xiàn)當前goroutine
再也沒有機會被喚醒時,則會panic
铣耘。
fatal error: all goroutines are asleep - deadlock!
通過帶default
的select
實現(xiàn)非阻塞讀寫洽沟,用于防止select
發(fā)生阻塞。
select{
default:
}
多個case
運行時select
會隨機公平地選出一個執(zhí)行蜗细,其它不會執(zhí)行裆操。若存在default
子句則會執(zhí)行該語句。若沒有default
則select
阻塞炉媒,直到某個通信可以運行踪区。Go不會重新對channel
或值進行求值。
當select
語句永遠阻塞吊骤,沒有其它goroutine
寫入此channel
時缎岗,將導致死鎖。
ch := make(chan int)
select{
case <-ch:
}
fatal error: all goroutines are asleep - deadlock!
若存在默認情況default
則不會發(fā)生死鎖deadlock
白粉,因為在沒有其它case
準備就緒時將執(zhí)行default
默認情況传泊。
ch := make(chan int)
select{
case <-ch:
default:
fmt.Println("default case executed")
}
例如:典型生產(chǎn)者消費者模式
func main(){
ch1 := make(chan int)
ch2 := make(chan int)
//生產(chǎn)者
go pump1(ch1)
go pump2(ch2)
//消費者
go suck(ch1, ch2)
time.Sleep(1e9)
}
-
ch1
和ch2
在無限循環(huán)中通過pump1()
和pump2()
填充整數(shù)
func pump1(ch chan int){
for i:=0; ; i++{
ch <- i * 1
}
}
func pump2(ch chan int){
for i:=0; ; i++{
ch <- i * 2
}
}
-
suck()
在無限循環(huán)中輪詢輸入項,通過select
語句獲取不同信道的整數(shù)并輸出鸭巴。
func suck(ch1,ch2 chan int){
for{
select{
case v := <-ch1:
fmt.Printf("[CH1] receive %d\n", v)
case v := <-ch2:
fmt.Printf("[CH2] receive %d\n", v)
default:
fmt.Printf("NO Communicating\n")
}
}
}
選擇select
的哪一個case
取決于哪個信道接收到了消息眷细。
timeout
當case
中的channel
始終沒有接收到數(shù)據(jù),同時也沒有提供default
語句時鹃祖,select
語句整體會發(fā)生阻塞溪椎。有時并不希望select
一直阻塞下去,此時可手動設置一個超時時間恬口。
func expire(ch chan bool, t int){
time.Sleep(time.Second * time.Duration(t))
ch <- true
}
func main(){
timeout := make(chan bool, 1)
go expire(timeout, 2)
ch1 := make(chan string, 1)
ch2 := make(chan string, 1)
select{
case msg1 := <-ch1:
fmt.Printf("[CH1] received: %s\n", msg1)
case msg2 := <-ch2:
fmt.Printf("[CH2] received: %s\n", msg2)
case <-timeout:
fmt.Printf("[EXPIRE] exit\n")
}
}
例如:使用select
實現(xiàn)channel
的讀取超時機制校读,不能使用default
否則3秒超時未到,就會直接執(zhí)行default
祖能。
timeout := make(chan bool, 1)
go func(){
time.Sleep(time.Second * 3)
timeout <- true
}()
ch := make(chan int)
select{
case <-ch:
case <-timeout:
fmt.Printf("TIMEOUT\n")
}
可使用time.After
實現(xiàn)超時控制
ch := make(chan int)
select{
case <-ch:
fmt.Printf("read from ch\n")
case <-time.After(time.Second * time.Duration(3)):
fmt.Printf("[TIMEOUT] exit\n")
}
for
當for
和select
結合時地熄,break
是無法跳出for
之外的,若需break
出來需添加標簽使用goto
芯杀,或break
到具體為止端考。
- 解決方案1:使用Golang中
break
的特性在外層for
上添加一個標簽 - 解決方案2:使用
goto
直接跳出循環(huán)到指定標記位置
- 對于
for
中空的select{}
也有可能會引起CPU占用過高的問題
ch := make(chan bool)
for i:=0; i<runtime.NumCPU(); i++{
go func(){
for{
select{
case <-ch:
break
default:
}
}
}()
}
time.Sleep(time.Second * 10)
for i:=0; i<runtime.NumCPU(); i++{
ch<-true
}
一般來說,使用select
監(jiān)聽各個case
的I/O事件揭厚,每個case
都是阻塞的却特。上例中原本希望select
在獲取到ch
里的數(shù)據(jù)時立即退出循環(huán),但由于在for
循環(huán)中筛圆,第一次讀取ch
后僅僅退出了select
但并未退出for
裂明,因此下次哈希繼續(xù)執(zhí)行select
邏輯,此時將永遠是執(zhí)行default
太援,直到ch
里讀取到數(shù)據(jù)闽晦。否則會一直在一個死循環(huán)中運行扳碍,因此即便只是放到一個goroutine
中運行,也會占滿所有的CPU仙蛉。解決的方式直接把default
拿掉笋敞,這樣select
會一直阻塞在ch
通道的I/O上,當ch
有數(shù)據(jù)時就可以隨時響應通道中的信息荠瘪。
select
實現(xiàn)了一種監(jiān)聽模式夯巷,通常用在(無限)循環(huán)中,在某中情況下可通過break
語句使循環(huán)退出哀墓。
ch := make(chan int)
//定時2s
ticker := time.NewTicker(time.Second * 2)
defer ticker.Stop()
//發(fā)送信號
go func(ch chan int){
time.Sleep(time.Second * 5)
ch <- 1
}(ch)
//監(jiān)聽I/O
for{
select{
case <-ticker.C:
fmt.Printf("task running...\n")
case result,ok := <-ch:{
if ok{
fmt.Printf("chan number is %v\n", result)
break
}
}
}
}
fmt.Printf("END\n")
例如:
ch := make(chan int)
quit := make(chan bool)
//寫數(shù)據(jù)
go func() {
//循環(huán)寫入
for i := 0; i < 5; i++ {
ch <- i
time.Sleep(time.Second)
}
//關閉channel
close(ch)
//通知主goroutine推出
quit <- true
//退出當前goroutine
runtime.Goexit()
}()
//主goroutine 讀數(shù)據(jù)
for {
select {
case num := <-ch:
fmt.Printf("received number is %d\n", num)
case <-quit:
fmt.Printf("quit\n")
//break //跳出select
return //終止進程
}
fmt.Printf("==================\n")
}
多路復用
select
是Golang在語言層面提供的多路I/O復用機制趁餐,它可檢測多個channel
是否ready
(是否可讀或可寫)。
select
是如何實現(xiàn)多路復用的篮绰,為什么沒有在第一個channel
操作時阻塞后雷,從而導致后面的case
都執(zhí)行不了。