持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第11天,点击查看活动详情
一、概述
select 是 Golang 中的一个控制结构,语法上类似于switch 语句,只不过select是用于 goroutine 间通信的 ,每个 case 必须是一个通信操作,要么是发送要么是接收,select 会随机执行一个可运行的 case。如果没有 case 可运行,goroutine 将阻塞,直到有 case 可运行。
二、基本语法
语法
//select基本用法
select {
case <- chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}
注意
-
如果没有
default分支,select会阻塞在多个channel上,对多个channel的读/写事件进行监控。 -
如果有一个或多个IO操作可以完成,则Go运行时系统会随机的选择一个执行,否则的话,如果有
default分支,则执行default分支语句,如果连default都没有,则select语句会一直阻塞,直到至少有一个IO操作可以进行。
三、Select使用
3.1 同时从多个通道中接收数据
import (
"fmt"
"time"
)
func main() {
//1.定义一个通道 10个数据int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
//2.定义一个通道 5个数据string
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
}
//使用select来获取channel里面的数据的时候不需要关闭channel
for {
select {
case v := <-intChan:
fmt.Printf("从 intChan 读取的数据%d\n", v)
time.Sleep(time.Millisecond * 50)
case v := <-stringChan:
fmt.Printf("从 stringChan 读取的数据%v\n", v)
time.Sleep(time.Millisecond * 50)
default:
fmt.Printf("数据获取完毕")
return //注意退出...
}
}
}
运行结果
从 stringChan 读取的数据hello0
从 stringChan 读取的数据hello1
从 stringChan 读取的数据hello2
从 intChan 读取的数据0
从 stringChan 读取的数据hello3
从 intChan 读取的数据1
从 stringChan 读取的数据hello4
从 intChan 读取的数据2
从 intChan 读取的数据3
从 intChan 读取的数据4
从 intChan 读取的数据5
从 intChan 读取的数据6
从 intChan 读取的数据7
从 intChan 读取的数据8
从 intChan 读取的数据9
数据获取完毕
3.2 造成死锁
当程序中不使用default 分支,如果一直没有命中其中的某个 case 最后会造成死锁。
import "fmt"
func main() {
// 创建三个通道
ch1 := make(chan string, 1)
ch2 := make(chan string, 1)
ch3 := make(chan string, 1)
select {
case message1 := <-ch1: // 如果从通道 1 收到数据
fmt.Println("ch1 received:", message1)
case message2 := <-ch2: // 如果从通道 2 收到数据
fmt.Println("ch2 received:", message2)
case message3 := <-ch3: // 如果从通道 3 收到数据
fmt.Println("ch3 received:", message3)
}
}
运行报错
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [select]:
main.main()
D:/Codes/gocode/godemo/base11/test02.go:11 +0xfd
运行上面的程序会造成死锁。解决该问题的方法是写好 default 分支。
空 select造成死锁
package main
func main() {
select {} //fatal error: all goroutines are asleep - deadlock!
}
空select 语句没有任何 case,因此它会一直阻塞,导致死锁。
3.3 select 超时处理
当 case 里的通道始终没有接收到数据时,而且也没有 default 语句时, select 整体就会阻塞,但是有时我们并不希望 select 一直阻塞下去,这时候就可以手动设置一个超时时间。
import (
"fmt"
"time"
)
func makeTimeout(ch chan bool, t int) {
time.Sleep(time.Second * time.Duration(t))
ch <- true
}
func main() {
c1 := make(chan string, 1)
c2 := make(chan string, 1)
c3 := make(chan string, 1)
timeout := make(chan bool, 1)
go makeTimeout(timeout, 2)
select {
case msg1 := <-c1:
fmt.Println("c1 received: ", msg1)
case msg2 := <-c2:
fmt.Println("c2 received: ", msg2)
case msg3 := <-c3:
fmt.Println("c3 received: ", msg3)
case <-timeout:
fmt.Println("Timeout, exit.")
}
}
运行结果
Timeout, exit.
总结
- select语句只能用于信道的读写操作;
- select中的case条件(非阻塞)是并发执行的,select会选择先操作成功的那个case条件去执行,如果多个同时返回,则随机选择一个执行,此时将无法保证执行顺序。对于阻塞的case语句会直到其中有信道可以操作,如果有多个信道可操作,会随机选择其中一个 case 执行;
- 对于case条件语句中,如果存在信道值为nil的读写操作,则该分支将被忽略,可以理解为从select语句中删除了这个case语句;
- 如果有超时条件语句,判断逻辑为如果在这个时间段内一直没有满足条件的case,则执行这个超时case。如果此段时间内出现了可操作的case,则直接执行这个case。一般用超时语句代替了default语句;
- 对于空的select{},会引起死锁;
- 对于for中的select{}, 也有可能会引起cpu占用过高的问题。
附参考文章链接