24. Select
什么是 select?
select
語(yǔ)句用于在多個(gè)發(fā)送/接收信道操作中進(jìn)行選擇此改。select
語(yǔ)句會(huì)一直阻塞,直到發(fā)送/接收操作準(zhǔn)備就緒。如果有多個(gè)信道操作準(zhǔn)備完畢另萤,select
會(huì)隨機(jī)地選取其中之一執(zhí)行。該語(yǔ)法與 switch
類(lèi)似诅挑,所不同的是四敞,這里的每個(gè) case
語(yǔ)句都是信道操作。我們好好看一些代碼來(lái)加深理解吧拔妥。
示例
package main
import (
"fmt"
"time"
)
func server1(ch chan string) {
time.Sleep(6 * time.Second)
ch <- "from server1"
}
func server2(ch chan string) {
time.Sleep(3 * time.Second)
ch <- "from server2"
}
func main() {
output1 := make(chan string)
output2 := make(chan string)
go server1(output1)
go server2(output2)
select {
case s1 := <-output1:
fmt.Println(s1)
case s2 := <-output2:
fmt.Println(s2)
}
}
在上面程序里忿危,server1
函數(shù)(第 8 行)休眠了 6 秒,接著將文本 from server1
寫(xiě)入信道 ch
没龙。而 server2
函數(shù)(第 12 行)休眠了 3 秒铺厨,然后把 from server2
寫(xiě)入了信道 ch
。
而 main
函數(shù)在第 20 行和第 21 行硬纤,分別調(diào)用了 server1
和 server2
兩個(gè) Go 協(xié)程解滓。
在第 22 行,程序運(yùn)行到了 select
語(yǔ)句咬摇。select
會(huì)一直發(fā)生阻塞伐蒂,除非其中有 case 準(zhǔn)備就緒。在上述程序里肛鹏,server1
協(xié)程會(huì)在 6 秒之后寫(xiě)入 output1
信道逸邦,而server2
協(xié)程在 3 秒之后就寫(xiě)入了 output2
信道。因此 select
語(yǔ)句會(huì)阻塞 3 秒鐘在扰,等著 server2
向 output2
信道寫(xiě)入數(shù)據(jù)缕减。3 秒鐘過(guò)后,程序會(huì)輸出:
from server2
然后程序終止芒珠。
select 的應(yīng)用
在上面程序中桥狡,函數(shù)之所以取名為 server1
和 server2
,是為了展示 select
的實(shí)際應(yīng)用皱卓。
假設(shè)我們有一個(gè)關(guān)鍵性應(yīng)用裹芝,需要盡快地把輸出返回給用戶(hù)。這個(gè)應(yīng)用的數(shù)據(jù)庫(kù)復(fù)制并且存儲(chǔ)在世界各地的服務(wù)器上娜汁。假設(shè)函數(shù) server1
和 server2
與這樣不同區(qū)域的兩臺(tái)服務(wù)器進(jìn)行通信嫂易。每臺(tái)服務(wù)器的負(fù)載和網(wǎng)絡(luò)時(shí)延決定了它的響應(yīng)時(shí)間。我們向兩臺(tái)服務(wù)器發(fā)送請(qǐng)求掐禁,并使用 select
語(yǔ)句等待相應(yīng)的信道發(fā)出響應(yīng)怜械。select
會(huì)選擇首先響應(yīng)的服務(wù)器颅和,而忽略其它的響應(yīng)。使用這種方法缕允,我們可以向多個(gè)服務(wù)器發(fā)送請(qǐng)求峡扩,并給用戶(hù)返回最快的響應(yīng)了。:)
默認(rèn)情況
在沒(méi)有 case 準(zhǔn)備就緒時(shí)障本,可以執(zhí)行 select
語(yǔ)句中的默認(rèn)情況(Default Case)教届。這通常用于防止 select
語(yǔ)句一直阻塞。
package main
import (
"fmt"
"time"
)
func process(ch chan string) {
time.Sleep(10500 * time.Millisecond)
ch <- "process successful"
}
func main() {
ch := make(chan string)
go process(ch)
for {
time.Sleep(1000 * time.Millisecond)
select {
case v := <-ch:
fmt.Println("received value: ", v)
return
default:
fmt.Println("no value received")
}
}
}
上述程序中彼绷,第 8 行的 process
函數(shù)休眠了 10500 毫秒(10.5 秒)巍佑,接著把 process successful
寫(xiě)入 ch
信道。在程序中的第 15 行寄悯,并發(fā)地調(diào)用了這個(gè)函數(shù)萤衰。
在并發(fā)地調(diào)用了 process
協(xié)程之后,主協(xié)程啟動(dòng)了一個(gè)無(wú)限循環(huán)猜旬。這個(gè)無(wú)限循環(huán)在每一次迭代開(kāi)始時(shí)脆栋,都會(huì)先休眠 1000 毫秒(1 秒),然后執(zhí)行一個(gè) select 操作洒擦。在最開(kāi)始的 10500 毫秒中椿争,由于 process
協(xié)程在 10500 毫秒后才會(huì)向 ch
信道寫(xiě)入數(shù)據(jù),因此 select
語(yǔ)句的第一個(gè) case(即 case v := <-ch:
)并未就緒熟嫩。所以在這期間秦踪,程序會(huì)執(zhí)行默認(rèn)情況,該程序會(huì)打印 10 次 no value received
掸茅。
在 10.5 秒之后椅邓,process
協(xié)程會(huì)在第 10 行向 ch
寫(xiě)入 process successful
。現(xiàn)在昧狮,就可以執(zhí)行 select
語(yǔ)句的第一個(gè) case 了景馁,程序會(huì)打印 received value: process successful
,然后程序終止逗鸣。該程序會(huì)輸出:
no value received
no value received
no value received
no value received
no value received
no value received
no value received
no value received
no value received
no value received
received value: process successful
死鎖與默認(rèn)情況
package main
func main() {
ch := make(chan string)
select {
case <-ch:
}
}
上面的程序中合住,我們?cè)诘?4 行創(chuàng)建了一個(gè)信道 ch
。我們?cè)?select
內(nèi)部(第 6 行)撒璧,試圖讀取信道 ch
透葛。由于沒(méi)有 Go 協(xié)程向該信道寫(xiě)入數(shù)據(jù),因此 select
語(yǔ)句會(huì)一直阻塞卿樱,導(dǎo)致死鎖僚害。該程序會(huì)觸發(fā)運(yùn)行時(shí) panic
,報(bào)錯(cuò)信息如下:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/tmp/sandbox416567824/main.go:6 +0x80
如果存在默認(rèn)情況殿如,就不會(huì)發(fā)生死鎖贡珊,因?yàn)樵跊](méi)有其他 case 準(zhǔn)備就緒時(shí),會(huì)執(zhí)行默認(rèn)情況涉馁。我們用默認(rèn)情況重寫(xiě)后门岔,程序如下:
package main
import "fmt"
func main() {
ch := make(chan string)
select {
case <-ch:
default:
fmt.Println("default case executed")
}
}
以上程序會(huì)輸出:
default case executed
如果 select
只含有值為 nil
的信道,也同樣會(huì)執(zhí)行默認(rèn)情況烤送。
package main
import "fmt"
func main() {
var ch chan string
select {
case v := <-ch:
fmt.Println("received value", v)
default:
fmt.Println("default case executed")
}
}
在上面程序中寒随,ch
等于 nil
,而我們?cè)噲D在 select
中讀取 ch
(第 8 行)帮坚。如果沒(méi)有默認(rèn)情況妻往,select
會(huì)一直阻塞,導(dǎo)致死鎖试和。由于我們?cè)?select
內(nèi)部加入了默認(rèn)情況讯泣,程序會(huì)執(zhí)行它,并輸出:
default case executed
隨機(jī)選取
當(dāng) select
由多個(gè) case 準(zhǔn)備就緒時(shí)阅悍,將會(huì)隨機(jī)地選取其中之一去執(zhí)行好渠。
package main
import (
"fmt"
"time"
)
func server1(ch chan string) {
ch <- "from server1"
}
func server2(ch chan string) {
ch <- "from server2"
}
func main() {
output1 := make(chan string)
output2 := make(chan string)
go server1(output1)
go server2(output2)
time.Sleep(1 * time.Second)
select {
case s1 := <-output1:
fmt.Println(s1)
case s2 := <-output2:
fmt.Println(s2)
}
}
在上面程序里,我們?cè)诘?18 行和第 19 行分別調(diào)用了 server1
和 server2
兩個(gè) Go 協(xié)程节视。接下來(lái)拳锚,主程序休眠了 1 秒鐘(第 20 行)。當(dāng)程序控制到達(dá)第 21 行的 select
語(yǔ)句時(shí)寻行,server1
已經(jīng)把 from server1
寫(xiě)到了 output1
信道上霍掺,而 server2
也同樣把 from server2
寫(xiě)到了 output2
信道上。因此這個(gè) select
語(yǔ)句中的兩種情況都準(zhǔn)備好執(zhí)行了拌蜘。如果你運(yùn)行這個(gè)程序很多次的話杆烁,輸出會(huì)是 from server1
或者 from server2
,這會(huì)根據(jù)隨機(jī)選取的結(jié)果而變化拦坠。
請(qǐng)?jiān)谀愕谋镜叵到y(tǒng)上運(yùn)行這個(gè)程序连躏,獲得程序的隨機(jī)結(jié)果。因?yàn)槿绻阍?playground 上在線運(yùn)行的話贞滨,它的輸出總是一樣的入热,這是由于 playground 不具有隨機(jī)性所造成的。
這下我懂了:空 select
package main
func main() {
select {}
}
你認(rèn)為上面代碼會(huì)輸出什么晓铆?
我們已經(jīng)知道勺良,除非有 case 執(zhí)行,select 語(yǔ)句就會(huì)一直阻塞著骄噪。在這里尚困,select
語(yǔ)句沒(méi)有任何 case,因此它會(huì)一直阻塞链蕊,導(dǎo)致死鎖事甜。該程序會(huì)觸發(fā) panic谬泌,輸出如下:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [select (no cases)]:
main.main()
/tmp/sandbox299546399/main.go:4 +0x20