select 的基本用法:select 可以让 Goroutine 同时等待多个 Channel 的可读或可写状态,然后执行对应的 case 语句。
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int) // 创建一个整数类型的通道
ch2 := make(chan int) // 创建另一个整数类型的通道
go func() {
time.Sleep(3 * time.Second) // 模拟耗时操作
ch1 <- 100 // 向ch1发送数据
}()
go func() {
time.Sleep(5 * time.Second) // 模拟耗时操作
ch2 <- 200 // 向ch2发送数据
}()
select {
case x := <-ch1: // 如果ch1有数据可接收,则执行这个分支
fmt.Println("Received", x)
case y := <-ch2: // 如果ch2有数据可接收,则执行这个分支
fmt.Println("Received", y)
default: // 如果没有任何case满足条件,则执行这个分支
fmt.Println("No data received")
}
}
在这个例子中,我们创建了两个整数类型的通道ch1和ch2,并分别在两个goroutine(轻量级线程)中向它们发送数据。然后我们使用select语句来监听这两个通道上是否有数据可接收。因为我们在发送数据之前都加了延时操作,所以当我们运行select语句时,没有任何case满足条件,所以会执行default分支打印"No data received"。
select 的数据结构:
select 的运行时结构包括 hselect 结构体、 scase 结构体和 sudog 结构体。hselect 结构体表示一个 select 操作,它包含了一个 scase 数组和一个 sudog 数组。scase 结构体表示一个 case 语句,它包含了一个 Channel 指针、一个元素指针、一个操作类型等字段。sudog 结构体表示一个等待在 Channel 上的 Goroutine,它包含了一个 Goroutine 指针、一个 scase 指针等字段。这些结构体之间通过链表或者数组来连接。
select 的实现原理:
select 的编译过程主要分为两步:第一步是生成汇编代码,这个过程会创建 hselect 和 scase 结构体,并将它们传递给 runtime.selectgo 函数;第二步是调用 runtime.selectgo 函数,这个函数会执行以下操作:首先检查是否有 Channel 已经就绪,如果有就随机选择一个执行对应的 case 语句;其次如果没有就绪的 Channel,就将当前 Goroutine 加入到每个 Channel 的等待队列中,并将当前 Goroutine 置为休眠状态;最后当有 Channel 就绪时,就唤醒对应的 Goroutine,并执行相应的 case 语句。
select 的优化策略:
Go 语言对 select 的一些优化技巧包括使用轮询算法来随机选择 case 语句、使用缓存池来复用 sudog 结构体等。轮询算法可以避免某些 case 语句被频繁选择而导致其他 case 语句被饿死;缓存池可以减少内存分配和垃圾回收的开销。