Go语言的select语句是并发编程中处理多个通道(channel)操作的关键工具。当select语句中存在多个可执行的case时,Go运行时会从中随机选择一个执行。这种“随机”并非完全的随机,而是基于特定算法实现的“伪随机”。本文将深入Go运行时源码,解析select语句的伪随机选择机制。
1. select语句基础
select语句允许你同时监听多个通道操作,当其中任意一个通道准备好时,select会执行对应的case分支。其基本语法如下:
select {
case <-ch1:
// ch1有数据可读
case ch2 <- data:
// ch2可写入数据
default:
// 没有通道准备好
}
当多个case同时满足条件时,select会随机选择一个执行。例如:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
ch1 <- 1
}()
go func() {
ch2 <- 2
}()
select {
case v1 := <-ch1:
fmt.Println("从ch1读取:", v1)
case v2 := <-ch2:
fmt.Println("从ch2读取:", v2)
}
}
多次运行上述代码,你会发现输出可能是“从ch1读取: 1”或“从ch2读取: 2”,这表明select确实进行了随机选择。
2. select的执行流程
select语句的执行并非由Go语言编译器直接生成,而是由Go运行时(runtime)负责处理。当编译器遇到select语句时,会生成对runtime.selectgo函数的调用。selectgo函数是select语句在运行时的核心实现,它负责监听所有通道,并根据情况选择一个case执行。
selectgo函数的执行流程大致如下:
- 准备阶段: 收集所有
case,包括case通道和default(如果存在)。 - 监听阶段: 尝试从所有非阻塞的
case中获取数据或发送数据。 - 选择阶段: 如果有多个
case同时满足条件,则从中随机选择一个。 - 执行阶段: 执行选中的
case对应的代码。
3. 伪随机选择机制的源码解析
要理解select的随机性,我们必须查看Go运行时的源码。select的核心逻辑位于runtime/select.go文件中,具体实现在selectgo函数内。
3.1 定位源码
selectgo函数的签名如下(Go 1.19版本):
// selectgo implements the select statement.
// cas0 points to an array of scase, and recvOK is an array of bool.
// The number of cases is the length of cas0.Elem().
// selectgo returns the selected case, and whether it was received or sent.
func selectgo(cas0 *unsafe.Pointer, order0 *uint16, n int, pc0 *uintptr) (int, bool)
3.2 scase结构体
scase是select语句中每个case的表示,其定义如下:
type scase struct {
c *hchan // channel
elem unsafe.Pointer // data element
}
在selectgo函数内部,所有case会被封装成scase数组。
3.3 随机性来源:scase数组的随机化
select的随机性并非来自复杂的随机数生成器,而是通过对scase数组进行简单的随机化处理实现的。具体来说,selectgo函数会先对所有case进行一轮遍历,找出所有可执行的case(即非阻塞的case)。如果只有一个case可执行,则直接返回该case。如果有多个case可执行,则会进入随机选择逻辑。
随机选择的核心逻辑如下(伪代码):
- 收集可执行的
case: 将所有可执行的case的索引存入一个临时数组selp。 - 随机化
selp数组: 对selp数组进行随机打乱(shuffle)。 - 选择第一个
case: 从随机化后的selp数组中取出第一个元素,作为最终选择的case。
Go运行时使用了一个简单的Fisher-Yates洗牌算法来随机化selp数组。这个算法会遍历数组,并随机交换元素,从而实现数组的随机排列。
3.4 Mermaid流程图:selectgo函数的case选择逻辑
以下Mermaid流程图展示了selectgo函数在选择case时的核心逻辑,特别是当多个case可执行时的随机化过程:
3.5 源码片段分析
以下是selectgo函数中与随机选择相关的关键代码片段(基于Go 1.19):
// runtime/select.go
func selectgo(cas0 *unsafe.Pointer, order0 *uint16, n int, pc0 *uintptr) (int, bool) {
// ... 省略部分代码 ...
// cas 是 scase 数组
cas := (*[1 << 16]scase)(unsafe.Pointer(cas0))[:n:n]
// order 是一个用于记录原始顺序的数组
order := (*[1 << 16]uint16)(unsafe.Pointer(order0))[:n:n]
// selp 存储可执行的case的索引
selp := make([]uint16, 0, n)
// pollorder 存储所有case的原始索引
pollorder := make([]uint16, n)
for i := 0; i < n; i++ {
pollorder[i] = uint16(i)
}
// 随机化 pollorder 数组, 这是第一次随机化
for i := range pollorder {
j := fastrandn(uint32(n - i))
pollorder[i], pollorder[i+j] = pollorder[i+j], pollorder[i]
}
// 遍历所有case, 找出可执行的case
for i := 0; i < n; i++ {
casg := cas[pollorder[i]]
// ... 检查case是否可执行 ...
if caseCanSelect(casg) {
selp = append(selp, pollorder[i])
}
}
// 如果只有一个可执行的case, 直接返回
if len(selp) == 1 {
return int(selp[0]), true
}
// 如果有多个可执行的case, 进行随机化
for i := range selp {
j := fastrandn(uint32(len(selp) - i))
selp[i], selp[i+j] = selp[i+j], selp[i]
}
// 返回随机化后的第一个case
return int(selp[0]), true
}
代码解析:
pollorder数组: 首先创建一个包含所有case原始索引的数组pollorder。- 第一次随机化: 使用
fastrandn函数(一个伪随机数生成器)对pollorder数组进行洗牌。这一步的目的是为了在后续遍历case时,能够以随机顺序检查它们。 - 收集可执行的
case: 遍历随机化后的pollorder数组,检查每个case是否可执行。将可执行的case的索引存入selp数组。 - 第二次随机化: 如果
selp数组中有多个元素(即有多个case可执行),则再次使用fastrandn函数对selp数组进行洗牌。 - 选择: 从随机化后的
selp数组中取出第一个元素,作为最终选择的case。
这种两次随机化的设计,确保了在多个case可执行时,选择是相对均匀和随机的。
4. 总结与关键点
select的随机性是伪随机的: Go的select语句并非使用复杂的密码学随机数生成器,而是通过简单的数组随机化算法实现的。- 随机性来源于
scase数组的随机化: 具体来说,是通过对可执行的case索引数组进行Fisher-Yates洗牌算法实现的。 - 实际影响: 这种伪随机性在并发编程中非常有用,它可以避免某些
case因为排在前面而总是被优先选择,从而可能导致其他case“饥饿”。然而,它并不适用于需要真正随机性的场景,如加密或安全相关的应用。

暂无评论,快来抢沙发吧!