1. select介绍
select
是 Go
语言中的一种控制结构,用于在多个通信操作中选择一个可执行的操作。它可以协调多个 channel
的读写操作,使得我们能够在多个 channel
中进行非阻塞的数据传输、同步和控制。
基本语法:
select {case communication clause :statement(s);case communication clause :statement(s);/* 你可以定义任意数量的 case */default : /* 可选 */statement(s);
}
如果多个 case 都可以运行,select 会随机公平地选出一个执行。如果没有 case 可以运行,它要么阻塞(等待 case),要么执行default子句。
2. select
语句的常用使用场景:
-
等待多个通道的消息(多路复用)
当我们需要等待多个通道的消息时,使用
select
语句可以非常方便地等待这些通道中的任意一个通道有消息到达,从而避免了使用多个goroutine进行同步和等待。 -
超时等待通道消息
当我们需要在一段时间内等待某个通道有消息到达时,使用
select
语句可以与time
包结合使用实现定时等待。 -
在通道上进行非阻塞读写
在使用通道进行读写时,如果通道没有数据,读操作或写操作将会阻塞。但是使用
select
语句结合default
分支可以实现非阻塞读写,从而避免了死锁或死循环等问题。
因此,select
的主要作用是在处理多个通道时提供了一种高效且易于使用的机制,简化了多个 goroutine
的同步和等待,使程序更加可读、高效和可靠。
3. 代码示例:
代码1:
package mainimport ("fmt""time"
)func main() {chan1 := make(chan int)chan2 := make(chan int)go func() {chan1 <- 1time.Sleep(5 * time.Second)}()go func() {chan2 <- 1time.Sleep(5 * time.Second)}()select {case <-chan1:fmt.Println("chan1")case <-chan2:fmt.Println("chan2")default:fmt.Println("default")}fmt.Println("main exit")
}
输出结果为:
可能会出现三种结果:
chan1
main exit
chan2
main exit
default
main exit
select 中的 case 执行顺序是随机的,如果某个 case 中的 channel 已经 ready,那么就会执行相应的语句并退 出 select 流程,如果所有 case 中的 channel 都未 ready,那么就会执行 default 中的语句然后退出 select 流程。
由于启动的协程和 select 语句并不能保证执行的顺序,所以也有可能 select 执行时协程还未向channel中写入数据,所以 select 直接执行 default 语句并退出。因此,程序有可能产生三种输出
代码2:
package mainimport ("fmt"
)func main() {chan1 := make(chan int)chan2 := make(chan int)go func() {close(chan1)}()go func() {close(chan2)}()select {case <- chan1:fmt.Println("chan1")case <- chan2:fmt.Println("chan2")}fmt.Println("main exit.")
}
select 会随机检测各 case 语句中 channel 是否 ready,注意已关闭的 channel 也是可读的,所以上述程序中select 不会阻塞,具体执行哪个 case 语句是随机的。
代码3:
package mainfunc main() {select {}
}
对于空的 select 语句,程序会被阻塞,确切的说是当前协程被阻塞,同时 Go 自带死锁检测机制,当发现当前协程再也没有机会被唤醒时,则会发生 panic。所以上述程序会 panic。
定时器实现定时任务的执行代码:
package mainimport ("fmt""time"
)func main() {fmt.Println("定时任务开始")// 创建一个每秒触发一次的定时器ticker := time.NewTicker(1 * time.Second)done := make(chan bool)go func() {for {select {case <-ticker.C:fmt.Println("执行定时任务")case <-done:ticker.Stop()return}}}()// 等待5秒time.Sleep(5 * time.Second)done <- truefmt.Println("定时任务结束")
}
结果:
定时任务开始
执行定时任务
执行定时任务
执行定时任务
执行定时任务
执行定时任务
定时任务结束
超时退出实现代码:
package mainimport ("fmt""time"
)func main() {timeout := 5 * time.Seconddone := make(chan bool)go func() {// 模拟耗时操作time.Sleep(2 * time.Second)done <- true}()select {case <-done:fmt.Println("Task completed successfully.")case <-time.After(timeout):fmt.Println("Timeout! The operation took too long.")}
}
或:
package mainimport ("context""fmt""time"
)func main() {timeout := 5 * time.Secondctx, cancel := context.WithTimeout(context.Background(), timeout)defer cancel()done := make(chan bool)go func() {// 模拟耗时操作time.Sleep(2 * time.Second)done <- true}()select {case <-done:fmt.Println("Task completed successfully.")case <-ctx.Done():fmt.Println("Timeout! The operation took too long.")}
}
4. 总结
-
select 语句中除 default 外,每个 case 操作一个channel,要么读要么写
-
select语句中除 default 外,各 case 执行顺序是随机的
-
select 语句中如果没有 default 语句,则会阻塞等待任一 case
-
select 语句中读操作要判断是否成功读取,关闭的 channel 也可以读取
select在 Go 语言的源代码中不存在对应的结构体,只是定义了一个 runtime.scase 结构体(在src/runtime/select.go)表示每个 case 语句(包含defaut):
// Select case descriptor.
// Known to compiler.
// Changes here must also be made in src/cmd/compile/internal/walk/select.go's scasetype.
type scase struct {c *hchan // chanelem unsafe.Pointer // data element
}
因为所有的非 default 的 case 基本都要求是对Channel的读写操作,所以 runtime.scase 结构体中也包含一个 runtime.hchan 类型的字段存储 case 中使用的 Channel,另一个字段 elem 指向 case 条件包含的数据的指针,如 case ch1 <- 1,则 elem 指向常量1.
编译器会对select有不同的case的情况进行优化以提高性能。首先,编译器对select没有case、有单case和单case+default的情况进行单独处理,这些处理或者直接调用运行时函数,或者直接转成对channel的操作,或者以非阻塞的方式访问channel,多种灵活的处理方式能够提高性能,尤其是避免对channel的加锁。
对最常出现的select有多case的情况,会调用runtime.selectgo()函数来获取执行case的索引
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool)
selectgo函数内部逻辑:
-
使用fastrandn算法把scases数组的索引重新编排顺序。
-
根据新的索引顺序对hchan进行堆排序来获取case的锁定顺序。(保证 n log n 时间和恒定的堆栈占用空间)
-
锁定所有channel。
-
遍历所有channel,判断是否有可读或者可写的,如果有,解锁channel,返回对应数据。
-
否则,判断有没有default,如果有,解锁channel,返回default对应scase。
-
否则,把当前groutian添加到所有channel的等待队列里,解锁所有channel,等待被唤醒。
-
被唤醒后,再次锁定所有channel
-
遍历所有channel,把g从channel等待队列中移除,并找到可操作的channel
-
如果对应的scase不为空,直接返回对应的值
-
否则循环此过程