什么是select?
select语句用于从多个发送/接收通道操作中进行选择。选择语句会阻塞,直到其中一个发送/接收操作准备好。如果多个操作都准备好了,就会随机选择其中一个。语法与switch类似,只是每个case语句都是一个通道操作。让我们直接进入一些代码,以便更好地理解。
例子
1 | package main |
在上面的程序中,第8行中的server1
函数休眠6秒,然后将文本从server1
写到通道ch
上。12行的server2
函数休眠3秒,然后从server2
写到通道ch
(两个ch
不同)。
主函数在第20行和第21行分别调用go Goroutine server1
和server2
。
在第22行,控制到达 select
语句。 select
语句阻塞,直到其中一个case
准备就绪。在我们上面的程序中,server1
的Goroutine在6秒后写到output1
通道,而server2在3秒后写到output2
通道。所以选择语句将阻塞3秒,并等待server2
Goroutine写到output2
通道。3秒后,程序打印:
1 | from server2 |
然后就终止了。
select的实际使用
将上述程序中的函数命名为server1
和server2
背后的原因是为了说明select的实际使用。
让我们假设我们有一个关键任务的应用程序,我们需要尽快将输出结果返回给用户。这个应用程序的数据库被复制并存储在世界各地的不同服务器上。假设函数server1
和server2
实际上是与2个这样的服务器进行通信。每个服务器的响应时间取决于每个服务器的负载和网络延迟。我们向两个服务器发送请求,然后使用 select
语句在相应的通道上等待响应。首先响应的服务器被选择,其他的响应被忽略。这样,我们就可以向多个服务器发送相同的请求,并向用户返回最快的响应 :) 。
Default case(默认情况)
select
语句中的默认情况是在其他情况都没有准备好的情况下执行的。这通常是为了防止选择语句被阻塞。
1 | package main |
在上面的程序中,第8行中的process
函数睡眠了10500毫秒(10.5秒),然后将process successful
写入ch
通道。这个函数在第15行被调用。
在同时调用process
Goroutine之后,在main
Goroutine中开始了一个无限的for循环。无限循环在每次迭代开始时都会休眠1000毫秒(1秒),然后执行选择操作。在最初的10500毫秒内,select
语句的第一个case
,即case v := <-ch:
将不会被准备好,因为process
Goroutine将在10500毫秒后才写到ch
通道。因此,default case
将在这段时间内被执行,程序将打印no value received
10次。
10.5秒后,第10行process
Goroutine将process successful
写入ch
通道。现在,select
语句的第一种情况将被执行,程序将打印received value: process successful
,然后它将终止。这个程序将输出:
1 | no value received |
死锁和默认情况
1 | package main |
在上面的程序中,我们已经在第4行创建了一个通道ch
。我们试图在第6行的select中读取这个通道。由于没有其他Goroutine写到这个通道,select语句将永远阻塞,因此将导致死锁。这个程序在运行时将出现报错,并有如下信息:
1 | fatal error: all goroutines are asleep - deadlock! |
如果有一个default
案例存在,这个死锁就不会发生,因为default
案例将在没有其他case
准备好的时候被执行。上面的程序是用下面的default
案例重写的。
1 | package main |
上面的程序将被打印出来。
1 | default case executed |
同样地,即使select只有nil
通道,default
情况也会被执行。
1 | package main |
在上面的程序中,ch
是nil
,我们试图从第8行的select中读取ch
。如果没有default
案例,select就会永远阻塞,造成死锁。由于我们在select里面有一个default
案例,它将被执行,程序将被打印。
1 | default case executed |
随机选择
当一个select
语句中的多个case
准备好了,其中一个将被随机执行。
1 | package main |
在上面的程序中,server1
和server2
的Goroutines分别在第18和19行被调用。然后,main
程序在第20行休眠1秒。当控制到达第21行的select语句时,from server1
已经从server1
写到output1
通道,from server2
已经从server2
写到output2
通道,因此select语句的两种case都可以执行。如果你多次运行这个程序,输出将在from server1
或 from server2
之间变化,这取决于随机选择的情况。
请在你的本地系统中运行此程序以获得这种随机性。如果这个程序在playground上运行,它将打印相同的输出,因为playground是确定性的。
Gotcha-Empty select
1 | package main |
你认为上面这个程序的输出会是什么?
我们知道select
语句会阻塞,直到它的一个case
被执行。在这种情况下,select
语句没有任何case
,因此它将永远阻塞,导致死锁。这个程序会出现以下的报错信息。
1 | fatal error: all goroutines are asleep - deadlock! |
祝你有个愉快的一天。