作用
Go 语言中 通过select可以监听channel上的数据流动。select 也能够让 Goroutine 同时等待多个 Channel 可读或者可写,在多个文件或者 Channel状态改变之前,select 会一直阻塞当前线程或者 Goroutine。
select的用法与switch语言非常类似,由select开始一个新的选择块,每个选择块条件由case语句来描述。
Execution of a “select” statement proceeds in several steps:
1. For all the cases in the statement, the channel operands of receive operations and the channel and right-hand-side expressions of send statements are evaluated exactly once, in source order, upon entering the “select” statement. The result is a set of channels to receive from or send to, and the corresponding values to send. Any side effects in that evaluation will occur irrespective of which (if any) communication operation is selected to proceed. Expressions on the left-hand side of a RecvStmt with a short variable declaration or assignment are not yet evaluated.
所有channel表达式都会被求值、所有被发送的表达式都会被求值。求值顺序:自上而下、从左到右.
结果是选择一个发送或接收的channel,无论选择哪一个case进行操作,表达式都会被执行。RecvStmt左侧短变量声明或赋值未被评估。
If one or more of the communications can proceed, a single one that can proceed is chosen via a uniform pseudo-random selection. Otherwise, if there is a default case, that case is chosen. If there is no default case, the “select” statement blocks until at least one of the communications can proceed.
如果有一个或多个IO操作可以完成,则Go运行时系统会随机的选择一个执行,否则的话,如果有default分支,则执行default分支语句,如果连default都没有,则select语句会一直阻塞,直到至少有一个IO操作可以进行.Unless the selected case is the default case, the respective communication operation is executed.
除非所选择的情况是默认情况,否则执行相应的通信操作。
4.I f the selected case is a RecvStmt with a short variable declaration or an assignment, the left-hand side expressions are evaluated and the received value (or values) are assigned.
如果所选case是具有短变量声明或赋值的RecvStmt,则评估左侧表达式并分配接收值(或多个值)。
5.The statement list of the selected case is executed.
执行所选case中的语句
[
](https://golang.org/ref/spec#Select_statements)
基本用法
select { // 不停的在这里检测
case <-chanl : // 检测有没有数据可以读
// 如果chanl成功读取到数据,则进行该case处理语句
case chan2 <- 1 : // 检测有没有可以写
// 如果成功向chan2写入数据,则进行该case处理语句
// 假如没有default,那么在以上两个条件都不成立的情况下,就会在此阻塞
// 一般default会不写在里面,select中的default子句总是可运行的,因为会很消耗CPU资源
default:
//如果以上都没有符合条件,那么则进行default处理流程
}
一个select语句中,Go会按顺序从头到尾评估每一个发送和接收的语句。
如果其中的任意一个语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。
如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况:
① 如果给出了default语句,那么就会执行default的流程,同时程序的执行会从select语句后的语句中恢复。
② 如果没有default语句,那么select语句将被阻塞,直到至少有一个case可以进行下去。
switch用法
package main
var i interface{}
func convert(i interface{}) {
switch t := i.(type) {
case int:
println("i is interger", t)
case string:
println("i is string", t)
case float64:
println("i is float64", t)
default:
println("type not found")
}
}
func main() {
i = 100
convert(i)
i = float64(45.55)
convert(i)
i = "foo"
convert(i)
convert(float32(10.0))
}
/**
打印结果
i is interger 100
i is float64 +4.555000e+001
i is string foo
type not found
*/
select的使用
select的随机性
package main
import (
"fmt"
"time"
)
func init() {
/**
select
作用于channel之上, 多路复用
select 会随机公平的选择一个case语句执行
*/
ch1 := make(chan int, 1)
ch2 := make(chan int, 2)
ch1 <- 1
ch2 <- 2
// 下面会随机执行一个case
select {
case data:= <-ch1 :
fmt.Println("random ", data)
case data:= <-ch2 :
fmt.Println("random ", data)
}
}
执行上面代码,有时会拿到 random 01 有时会拿到 random 02,这就是 select 的特性之一,case 是随机选择。所以當 select 有两个channel 以上时,如果同时对全部 channel 发送数据,则会随机选择不同的channel。
避免造成死锁
拿上面的列子,不向channel中写入数据, Channel 就会造成 panic, deadlock!!!
func init() {
ch1 := make(chan int, 1)
ch2 := make(chan int, 2)
// 不向channel中写入数据
//ch1 <- 1
//ch2 <- 2
// 下面会随机执行一个case
select {
case data:= <-ch1 :
fmt.Println("random ", data)
case data:= <-ch2 :
fmt.Println("random ", data)
}
}
打印结果
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [select, locked to thread]:
main.init.0()
/select.go:22 +0xe5
Process finished with the exit code 2
解决这个问题的方法有两种
在 select 的时候,也写好 default 分支代码,default 下可以没有写任何代码。
func init() {
ch1 := make(chan int, 1)
ch2 := make(chan int, 2)
//ch1 <- 1
//ch2 <- 2
// 下面会随机执行一个case
select {
case data:= <-ch1 :
fmt.Println("random ", data)
case data:= <-ch2 :
fmt.Println("random ", data)
default:
fmt.Println("exit")
// 也可以什么都不写
// default:
}
}
// 不会报错,只会执行 default
主协程 main 不会因为channle 而造成deadlock。
select 超时处理
在 并发编程 的通信过程中,经常会遇到超时问题,即向 channel 写数据时发现 channel 已满,或者从 channel 试图读取数据时发现 channel 为空。
在 select 中,我们可以使用 time.After 来实现 select 的超时控制,同时,我们还可以使用 break 语句,来结束 select 语句,就像我们之前结束 for 循环 一样。
当 case 里的信道始终没有接收到数据时,而且也没有 default 语句时,select 整体就会阻塞,但是有时我们并不希望 select 一直阻塞下去,这时候就可以手动设置一个超时时间。
语法
select{
case operator1:
//statement1
case oprtator2:
//statement2
case <-time.After(second * time.Second):
//timeout
}
说明
当 operator1 和 operator2 这两个 case 都不满足时,且等待 second 秒后,会触发 time.After case,通过该 case 可以避免 select 一直等到而阻塞进程。
func main() {
timeout := make(chan bool, 1)
go func() {
time.Sleep(2 * time.Second) //2秒
timeout <- true
}()
ch := make(chan int)
select {
case <-ch:
case <-timeout:
fmt.Println("timeout 01") // 超时2秒打印
case <-time.After(time.Second * 1): // 超时1秒打印
fmt.Println("timeout 02")
}
}
// 1s后打印 timeout 02
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
quit := make(chan bool)
go func() {
for {
select {
// 有数据写入就会打印
case num := <- ch :
fmt.Println("received num: ", num)
// 上面的ch如果一直没数据会阻塞,那么select也会检测其他case条件,检测到后3秒超时
case <- time.After(3*time.Second):
fmt.Println("Timeout")
quit <- true
}
}
}()
for i:=0; i<3; i++ {
// 向管道内写入3个数据 0 1 2 后,不再写入,子协程没有数据读取
ch <- i
time.Sleep(time.Second)
}
// 暂时阻塞,main 协程接收到 quit 消息,自动退出
<- quit
fmt.Println("Over")
/*
received num: 0
received num: 1
received num: 2
Timeout
Over
*/
}
检查 channel 是否已满
func main() {
ch := make(chan int, 1)
ch <- 1
select {
case ch <- 2:
fmt.Println("channel value is", <-ch)
fmt.Println("channel value is", <-ch)
default:
fmt.Println("channel blocking")
}
}
// 打印channel blocking
// 把ch := make(chan int, 1) 1改成2 打印
// channel value is 1
// channel value is 2
select for 用法
如果有多个 channel 需要读取,而读取是不间断的,就必须使用 for + select 机制来实现
package main
import (
"fmt"
"time"
)
func main9() {
ch1 := make(chan int, 1)
ch2 := make(chan int, 2)
//ch1 <- 1
//ch2 <- 2
// 下面会随机执行一个case
select {
case data:= <-ch1 :
fmt.Println("random ", data)
case data:= <-ch2 :
fmt.Println("random ", data)
default:
fmt.Println("exit")
}
}
// select的作用 超时处理
func main22() {
timeout := false
go func() {
time.Sleep(time.Second*2)
timeout = true
}()
for {
if timeout {
fmt.Println("Over")
break
}
time.Sleep(time.Millisecond*10)
}
}
func main3() {
timeout := make(chan bool, 1)
go func() {
time.Sleep(2 * time.Second) //2秒
timeout <- true
}()
ch := make(chan int)
select {
case <-ch:
case <-timeout:
fmt.Println("timeout 01") // 超时2秒打印
case <-time.After(time.Second * 1): // 超时1秒打印
fmt.Println("timeout 02")
}
}
// 检查通道是否已满
func main7() {
ch := make(chan int, 2)
ch <- 1
select {
case ch <- 2:
fmt.Println("channel value is", <-ch)
fmt.Println("channel value is", <-ch)
default:
fmt.Println("channel blocking")
}
}
// 多个select
func main() {
i := 0
ch := make(chan string, 0)
defer func() {
// 关闭通道
close(ch)
}()
go func() {
for {
time.Sleep(1 * time.Second)
fmt.Println(time.Now().Unix())
i++
select {
case m := <-ch:
println(m)
break
default:
}
}
}()
time.Sleep(time.Second * 4)
ch <- "stop"
}
// 打印结果
// 1638714220
// 1638714221
// 1638714222
// 1638714223
// stop
总结
- select 只能用于 channel 的操作(写入/读出),而 switch 则更通用一些;
- select 的 case 是随机的,而 switch 里的 case 是顺序执行;
- select 要注意避免出现死锁,同时也可以自行实现超时机制;
- select 里没有类似 switch 里的 fallthrough 的用法;
- select 不能像 switch 一样接函数或其他表达式。
- select 监听的case中,没有满足条件的就阻塞
- select 多个满足条件的就任选一个执行
- select本身不带循环,需要外层的for
- default通常不用,会产生忙轮询
- break只能跳出select中的一个case
参考