导言
- 原文链接: Part 24: Select
- If translation is not allowed, please leave me in the comment area and I will delete it as soon as possible.
Select
select
是什么?
select
语句 用于从多个通道中选择操作,并执行。select
语句 会有如下的可能:
select
会处于阻塞状态,直到 发送/接收 操作之一准备就绪。- 如果有一个操作准备就绪,
select
会马上执行它。 - 如果有多个操作准备就绪,
select
会随机的挑选出一个,并执行。
select
的句式与 switch
是类似的,区别在于:select
的每个 case
都只能是通道操作。
接下来,通过下面的代码,我们来更好的理解 select
。
实例
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)
}
}
在上面程序的第 8
行,server1
函数 休眠 6
秒后,将 from server1
写入通道。而在第 12
行,server2
函数休眠 3
秒后,也将 from server2
写入通道。
在第 20
、21
行,main
函数 分别调用两个协程:server1
、server2
。
在第 22
行,控件到达 select
语句。select
会阻塞,直到它的case
之一准备就绪。在上面的程序中,6
秒后,server1
会把 "from server1
写入 通道output1
中,而 server2
在 3
秒时,就已经把 from server2
写入 通道output2
中了。所以,通道只会阻塞 3
秒 — 等待 server2
协程 将信息写入 output2
通道 中。最终输出如下:
from server2
随后,程序结束。
select
的实际使用
在上面的代码中,之所以把函数命名为 server1
、server2
,是为了说明 select
的实际用法。
假设我们有一个应用程序,它需要将输出尽快地展示给用户。这个程序的数据库分布在世界上的不同地点,数据库内容都是一样的。假如,server1
和 server2
正在与 2
个数据库服务器通信。每个服务器的响应时间,取决于当前的负载及网络延迟。我们向这 2
个服务器发送请求后,使用 select
语句,在相应的通道上等待响应。首次响应将会被 select
所捕获,之后的响应都会被忽略。通过这种方式,我们就可以给多个服务器,发送相同的请求,并将最快的响应呈现给用户。
默认 case
当没有 case
准备就绪时,select
会执行默认的 case
。默认case
能避免 select
阻塞。
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
函数 会在休眠 10500
毫秒后,将 process successful
写入 通道ch
中。
在 15
行,我们调用了这个函数。在调用 process
协程 后,我们 在 main
协程 中开启了一个死循环。这个循环会先休眠 1000
毫秒,之后执行 select
操作,循环往复。
在 10500
毫秒前,case v:= <-ch
并没有准备好,因为此时 process
协程 处于休眠状态。因此,在 process
协程 休眠的这段时间,默认case
会被执行 10
次,程序会输出 10
次 no value received
。
在 10.5
秒后,process
协程 休眠结束,并将 process successful
写入 通道ch
。此时,select
语句 的第一个 case
准备就绪,于是,程序会打印 received value: process successful
。随后,程序终止。
程序的所有输出如下:
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
死锁 与 默认case
package main
func main() {
ch := make(chan string)
select {
case <-ch:
}
}
在上面程序的第 4
行,我们创建了一个 通道ch
。在第 6
行,select
试图从这个通道读取数据。此时,select
将会永远阻塞,因为没有其他协程向 通道ch
写入数据。于是,死锁出现了。
运行这个程序,会出现如下的异常信息:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/tmp/sandbox416567824/main.go:6 +0x80
如果存在 默认case
,此时就不会出现死锁,因为当没有 case
准备就绪时,默认case
会被执行。
我们使用 默认case
,重写上面的代码。
package main
import "fmt"
func main() {
ch := make(chan string)
select {
case <-ch:
default:
fmt.Println("default case executed")
}
}
输出如下:
default case executed
类似地,即使 select
中只有 nil
通道,默认case
也会被执行。
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")
}
}
在上面程序的第 8
行,通道ch
为 nil
,我们试着在它内部读取数据。如果 默认case
不存在,select
将会一直阻塞,产生死锁。由于在 select
中有 默认case
,默认case
将被执行。
程序输出如下:
default case executed
随机选择
当 select
内部有多个 case
准备就绪,select
只会随机的挑选其中一个执行。
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)
}
}
在上面程序的第 18
、19
行,我们分别开启了 server1
server2
协程。在第 20
行,main
函数 休眠了 1
秒。当控件到达第 21
行时,server1
和 server2
都已将数据写入对应的通道,即此时 select
中有多个 case
准备就绪。
运行上面的程序,你会发现:每次的输出不是固定的,输出在 from server1
和 from server2
之中徘徊。
为了观察随机性,请在你的本机上运行上面的程序。
疑难杂症 — 空 select
package main
func main() {
select {}
}
猜猜上面程序的输出。
我们已经知道:select
会阻塞,直到它的任意一个 case
被执行。而在上面的情况中, select
没有任何的 case
,因此,select
将会永远阻塞,从而导致死锁。
运行这个程序,程序会有如下异常:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [select (no cases)]:
main.main()
/tmp/sandbox299546399/main.go:4 +0x20
全剧终!
祝你暴富~
原作者留言
优质内容来之不易,您可以通过该 链接 为我捐赠。
最后
感谢原作者的优质内容。
这是我的第八次翻译,欢迎指出文中的任何错误。