1. 為什么需要多路復(fù)用
Go 程序在并發(fā)處理一些任務(wù)的時(shí)保礼,會為每一個(gè)任務(wù)創(chuàng)建一個(gè) goroutine奶躯,然后需要根據(jù)不同的 goroutine 的返回的結(jié)果做不同的處理。
如果按照通常的做法,分別獲取每個(gè) channel 的結(jié)果:
taskCh1 := make(chan bool)
taskCh2 := make(chan bool)
taskCh3 := make(chan bool)
go run(taskCh1)
go run(taskCh2)
go run(taskCh3)
for {
// 接收通道 1 的結(jié)果
result1 := <-taskCh1
// 接收通道 2 的結(jié)果
result2 := <-taskCh2
// 接收通道 3 的結(jié)果
result3 := <-taskCh3
}
然后再根據(jù)不同 goroutine 返回的結(jié)果做后續(xù)的處理柬祠,這個(gè)代碼有個(gè)問題蘸拔,需要等待所有的 goroutine 都執(zhí)行完成之后才能做出結(jié)果师郑,這樣實(shí)現(xiàn)的效率很低,因?yàn)槊恳粋€(gè)獲取 channel 值的過程都是阻塞的调窍。
在處理多個(gè)通道時(shí)宝冕,想同時(shí)接收多個(gè)通道的數(shù)據(jù)將會變的很困難。
而且在一些情況下邓萨,需要根據(jù)先返回通道的做出不同的處理地梨,上面那種方式無法做到,這就需要使用多路復(fù)用缔恳。
Go 提供了 select 機(jī)制來解決這個(gè)問題宝剖。
2. select 基本使用
select 語法形式和 switch 很相似,switch 接收一個(gè)變量歉甚,然后根據(jù)變量的值做不同的處理诈闺,select 操作接收的是通道操作:
ch := make(chan int, 1) // 這個(gè)例子中,這里必須用緩沖通道
for {
select {
case <-ch:
time.Sleep(time.Second)
fmt.Println("case 1 invoke")
case data := <-ch:
time.Sleep(time.Second)
fmt.Printf("case 2 invoke %d\n", data)
case ch <- 100:
time.Sleep(time.Second)
fmt.Println("case3 invoke")
}
在 select 的 case 中铃芦,可以執(zhí)行三種操作:
<- ch:接收通道雅镊,但是對值不處理
data := <-ch:接收通道,并處理從通道中得到的結(jié)果
ch <- 100:向通道中發(fā)送數(shù)據(jù)
上面的程序運(yùn)行起來之后刃滓,case 3 會首先執(zhí)行仁烹,然后 case1 和 case2 會隨機(jī)執(zhí)行一個(gè),程序就這樣一直交替運(yùn)行下去咧虎。
如果用 select 改造上面第一個(gè)例子中的代碼卓缰,就是下面這樣:
for {
select {
// 接收通道 1 的結(jié)果
case r := <-taskCh1:
fmt.Printf("task1 result %+v\n", r)
// 接收通道 2 的結(jié)果
case r := <-taskCh2:
fmt.Printf("task2 result %+v\n", r)
// 接收通道 3 的結(jié)果
case r := <-taskCh3:
fmt.Printf("task3 result %+v\n", r)
}
}
select 會及時(shí)響應(yīng)每一個(gè)就緒的 channel,無論是發(fā)送數(shù)據(jù)還是接收數(shù)據(jù)。
3. 處理超時(shí)情況
select 除了用于同時(shí)處理多個(gè)通道之外征唬,還可以用來處理一些通道超時(shí)的情況捌显,通道在阻塞的時(shí)候,如果沒有外界的干擾总寒,會一直等下去扶歪,但是可以通過 select 設(shè)置一個(gè)超時(shí)時(shí)間,來打斷阻塞:
ch := make(chan int, 1)
select {
case data := <- ch:
fmt.Printf("case invoke %+v\n", data)
case <-time.After(3 * time.Second):
fmt.Println("channel timeout")
}
上面的代碼在創(chuàng)建了一個(gè)通道摄闸,但沒有向通道中發(fā)送數(shù)據(jù)善镰,如果不用 select,程序就會死鎖年枕。
select 中添加了兩個(gè) case炫欺,一個(gè)從通道中獲取數(shù)據(jù), 但肯定獲取不到熏兄,所以在 3 秒鐘之后品洛,另一個(gè) case 就會執(zhí)行,返回通道超時(shí)的提示摩桶,這樣就避免了程序會一直等待下去毫别。
還有一個(gè)情況是我們有時(shí)候需用通過鍵盤獲取其他輸入設(shè)備向程序發(fā)送信號,也可以通過這種方式來實(shí)現(xiàn)典格,把上面的程序再修改一下:
ch := make(chan int, 1)
quitCh := make(chan bool, 1)
go func(ch chan bool) {
var quit string
fmt.Printf("quit? are you sure?: ")
fmt.Scanln(&quit)
quitCh <- true
}(quitCh)
select {
case data := <- ch:
fmt.Printf("case invoke %+v\n", data)
case <-quitCh:
fmt.Println("program quit")
}
這次不再通過超時(shí)來控制岛宦,而是通過鍵盤來控制,新建了一個(gè)通道耍缴,只有在鍵盤輸入之后砾肺,才會向通道中發(fā)送數(shù)據(jù),這樣就可以做到自由控制程序的退出防嗡。
4. 非阻塞的 select
在上面的示例代碼中变汪,其實(shí)還少寫了一部分,看下面的代碼:
ch := make(chan int)
for {
select {
case <-ch:
fmt.Println("case invoke")
}
}
上面的代碼會出現(xiàn)死鎖蚁趁,因?yàn)檫@個(gè) select 只有一個(gè) case裙盾,而這個(gè) case 永遠(yuǎn)都不會接收到數(shù)據(jù),所以 select 本身也被阻塞了他嫡,程序無法繼續(xù)運(yùn)行番官,就會造成死鎖,對于這種情況钢属,我們設(shè)置一個(gè)可用的 case徘熔,讓 select 變成非阻塞,就可以解決這個(gè)問題淆党。
ch := make(chan int)
for {
select {
case <-ch:
fmt.Println("case invoke")
default:
time.Sleep(time.Second)
fmt.Println("default invoke")
}
}
這樣酷师,程序就不會死鎖讶凉,而是不斷的執(zhí)行 default 中的內(nèi)容。
5. 小結(jié)
在這篇文章中山孔,我們介紹了通道的多路復(fù)用懂讯,并說明了可以用到多路復(fù)用的場景。下篇文章中台颠,我們來詳細(xì)聊一下 Go 是如何實(shí)現(xiàn)傳統(tǒng)的并發(fā)模型褐望。
本文轉(zhuǎn)自:https://juejin.cn/post/6970302437571706911