Golang中的select语句是控制并发的利器,可以作为多路复用器同时阻塞并等待多个发送或接收操作,它包含以下特性:
-
在select中任意一个操作被解除阻塞之前,整个select语句作为一个整体阻塞。
-
如果select中有多个case可以执行,那么将随机选择其中一个执行。
以下是select语句的一般形式。和switch语句稍微有点相似,也会有几个case和default分支。每一个case代表一个通信操作,可以在某个channel上进行发送或者接收,并且会包含一些语句组成的一个语句块。一个接收表达式可能只包含接收表达式自身,比如第一个case,或者包含在一个简短的变量声明类似第二个case一样;第二种形式能够引用接收到的值。
select {
case <-channel1:
// ...
case x := <-channel2:
// ...使用变量x
case channel3 <- y:
// ...
default:
// ...
}
select将会一直等待,直到case中有某个条件能够执行为止。当条件满足时,select才会去某个channel中取数据并执行case之后的语句;这时候其它channel是不会执行的。一个没有任何case的select语句写作select{},将会永远地阻塞。
// 在ch1或ch2通道上有可用数据为止,以下处理逻辑将始终保持阻塞。
select{
case<-ch1:
fmt.Println("Receivedfrom ch1")
case<-ch2:
fmt.Println("Receivedfrom ch2")
}
如果在nil通道上执行发送或接收操作将导致永久阻塞。这可以用来禁用select语句中的某些通道,如下所例:
ch1= nil // 通过设置为nil来禁用该通道
select{
case<-ch1:
fmt.Println("Receivedfrom ch1") // 永远不会被执行
case<-ch2:
fmt.Println("Receivedfrom ch2")
}
Default分支
如果所有其他case都被阻塞,则默认的default分支始终能够继续运行。样例如下:
// 以下代码逻辑永远不会被阻塞
select{
casex := <-ch:
fmt.Println("Received",x)
default:
fmt.Println("Nothingavailable")
}
多路复用器select示例
示例1:无限随机二进制序列
作为一个简单的游戏示例,以下代码可以基于select随机选择的特性产生0和1的随机二进制序列。
rand:= make(chan int, largeBuffer)
for{
select {
caserand <- 0: // no statement
caserand <- 1:
}
}
示例2:带有超时的阻塞操作
函数time.After是标准库的一部分;它将会等待指定的时间,然后在返回的通道上发送当前时间。
select{
casenews := <-AFP:
fmt.Println(news)
case<-time.After(time.Minute):
fmt.Println("Timeout: No news in one minute")
}
示例3:一个永远阻塞的语句
以下的select语句将永远阻塞,因为没有任务case可以继续向下执行。
select{}
在一些多线程程序中,通常是用在main函数的末尾。当main返回时,程序退出。
总结
本质上select的case语句,都是对应一个channel的I/O操作。select思想来源于网络IO模型,本质上也是IO多路复用,只不过这里的IO是基于channel的。在使用select机制时需要注意一些关键点,比如default子句是否需要设置,是否对通道关闭的情况进行了合理配置,是否透彻地理解了select的随机性等。
select的特性再次回顾:
-
每个case语句中必须包含或指向一个通道。
-
每个case语句中针对通道的表达式都会被求值。
-
每个case中所有向通道发送数据的表达式都会被求值。
-
当某个case中的通道可用时,将会执行对应的statement,此时其他case语句将被忽略。
-
当存在多个case都可以运行时,select会随机地选择一个分支执行。未被选中的分支将不会被执行。
-
如果没有任务case可以执行,并且分支中存在default子句,此时执行default分支。
-
如果没有default字句,整个select将阻塞,直到某个通道可用为止。此外,Go不会重新对通道进行求值。