select case底层机制
1. Select 基本架构
1.1 概述
Go 语言的 select 语句是处理多个 channel 操作的强大工具,它能够让一个 goroutine 等待多个通信操作。本文将深入分析 Go runtime 中 select 的底层实现机制,基于 go/src/runtime/select.go 源码。
1.2 核心数据结构
1.2.1 scase 结构体
select case 的核心数据结构是 scase,定义在 go/src/runtime/select.go 中:
// 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 // chan
elem unsafe.Pointer // data element
}
字段详解:
c: 指向 channel 的指针elem: 指向数据元素的指针(发送时指向要发送的数据,接收时指向接收缓冲区)
1.2.2 selectDir 枚举类型
// These values must match ../reflect/value.go:/SelectDir.
type selectDir int
const (
_ selectDir = iota
selectSend // case Chan <- Send
selectRecv // case <-Chan:
selectDefault // default
)
枚举值说明:
selectSend: 发送操作case ch <- valueselectRecv: 接收操作case <-ch或case val := <-chselectDefault: 默认分支default
1.3 Select 执行流程图
graph TD
Start[开始: select 语句] --> Compile[编译器分析]
Compile --> GenCases[生成 scase 数组]
GenCases --> CallSelectGo[调用 selectgo]
CallSelectGo --> CheckNil[过滤 nil channel]
CheckNil --> Permute[随机化轮询顺序]
Permute --> Sort[按地址排序锁定顺序]
Sort --> Lock[锁定所有 channel]
Lock --> Pass1[第一轮扫描: 查找就绪操作]
Pass1 --> Found{找到就绪操作?}
Found -->|是| Execute[执行操作]
Found -->|否| Block{是否阻塞模式?}
Block -->|否| Unlock[解锁所有 channel]
Unlock --> ReturnDefault[返回 default]
Block -->|是| Pass2[第二轮: 注册等待]
Pass2 --> Park[gopark 阻塞]
Park --> Wakeup[被唤醒]
Wakeup --> Pass3[第三轮: 清理并获取结果]
Execute --> Return[返回结果]
ReturnDefault --> Return
Pass3 --> Return
style Found fill:#87CEEB
style Block fill:#FFB6C1
style Execute fill:#90EE90
style Park fill:#DDA0DD
1.4 Select 的三种执行分支
graph LR
subgraph "Select 执行分支"
A[立即执行模式]
B[默认分支模式]
C[阻塞等待模式]
A --> A1[有就绪的 channel<br/>立即执行对应操作]
B --> B1[无就绪 channel<br/>有 default 分支<br/>执行 default]
C --> C1[无就绪 channel<br/>无 default 分支<br/>阻塞等待]
end
style A1 fill:#90EE90
style B1 fill:#87CEEB
style C1 fill:#FFB6C1
2. Select 核心函数 selectgo
2.1 函数签名和参数
基于 go/src/runtime/select.go 中的 selectgo 函数:
// selectgo 实现 select 语句
// cas0 指向 [ncases]scase 类型的数组,order0 指向 [2*ncases]uint16 类型的数组
// 其中 ncases 必须 <= 65536。两者都位于 goroutine 的栈上(无论 selectgo 中是否有逃逸)
// 对于 race detector 构建,pc0 指向 [ncases]uintptr 类型的数组(也在栈上);
// 对于其他构建,它被设置为 nil。
// selectgo 返回所选 scase 的索引,该索引与其各自的 select{recv,send,default}
// 调用的序数位置匹配。此外,如果所选 scase 是接收操作,它会报告是否接收到值。
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
参数详解:
cas0: 指向 scase 数组的指针,包含所有 caseorder0: 指向 uint16 数组的指针,用于存储轮询和锁定顺序pc0: race detector 使用的程序计数器数组nsends: 发送操作的数量nrecvs: 接收操作的数量block: 是否为阻塞模式(有 default 则为 false)
返回值:
int: 被选中的 case 索引(-1 表示 default)bool: 接收操作是否成功(仅接收操作有意义)
2.2 selectgo 源码分析
2.2.1 初始化和参数处理
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
if debugSelect {
print("select: cas0=", cas0, "\n")
}
// NOTE: In order to maintain a lean stack size, the number of scases
// is capped at 65536.
// 注意:为了维持精简的栈大小,scase 的数量被限制在 65536 个
cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0))
order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0))
ncases := nsends + nrecvs // 总的 case 数量
scases := cas1[:ncases:ncases] // scase 切片
pollorder := order1[:ncases:ncases] // 轮询顺序数组
lockorder := order1[ncases:][:ncases:ncases] // 锁定顺序数组
// NOTE: pollorder/lockorder's underlying array was not zero-initialized by compiler.
// 注意:pollorder/lockorder 的底层数组没有被编译器零初始化
// Even when raceenabled is true, there might be select
// statements in packages compiled without -race (e.g.,
// ensureSigM in runtime/signal_unix.go).
// 即使启用了 race 检测,也可能有在没有 -race 编译的包中的 select 语句
var pcs []uintptr
if raceenabled && pc0 != nil {
pc1 := (*[1 << 16]uintptr)(unsafe.Pointer(pc0))
pcs = pc1[:ncases:ncases]
}
casePC := func(casi int) uintptr {
if pcs == nil {
return 0
}
return pcs[casi]
}
// 性能分析支持
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
为了更好地理解 selectgo 的参数和内部数据结构,我们通过一个具体例子来说明:
// 示例代码
func example() {
ch1 := make(chan int, 1) // 有缓冲 channel
ch2 := make(chan string) // 无缓冲 channel
ch3 := make(chan bool) // 无缓冲 channel
select {
case ch1 <- 42: // case 0: 发送操作
println("sent to ch1")
case ch2 <- "hello": // case 1: 发送操作
println("sent to ch2")
case val := <-ch3: // case 2: 接收操作
println("received from ch3:", val)
default: // case 3: 默认分支
println("default case")
}
}
编译器分析结果:
nsends = 2(ch1 <- 42, ch2 <- "hello")nrecvs = 1(<-ch3)ncases = nsends + nrecvs = 3(不包含 default)block = false(有 default 分支)
数据结构详细分析:
cas1 和 scases 数组
cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0))
scases := cas1[:ncases:ncases] // 长度为 3 的切片
具体内存布局对比:
graph LR
subgraph "编译器视角"
A1["cas0 指向:<br/>[3]scase"]
A2["scase[0]: {ch1, &42}"]
A3["scase[1]: {ch2, &'hello'}"]
A4["scase[2]: {ch3, &val}"]
A1 --> A2
A2 --> A3
A3 --> A4
end
subgraph "selectgo 视角"
B1["cas1 指向:<br/>[65536]scase"]
B2["scase[0]: {ch1, &42}"]
B3["scase[1]: {ch2, &'hello'}"]
B4["scase[2]: {ch3, &val}"]
B5["scase[3]: {未使用}"]
B6["...<br/>scase[65535]: {未使用}"]
B1 --> B2
B2 --> B3
B3 --> B4
B4 --> B5
B5 --> B6
end
subgraph "切片视图"
C1["scases = cas1[:3:3]"]
C2["只能访问前3个元素"]
C3["长度: 3<br/>容量: 3"]
C1 --> C2
C2 --> C3
end
A2 -.->|同一内存| B2
A3 -.->|同一内存| B3
A4 -.->|同一内存| B4
style A1 fill:#FFB6C1
style B1 fill:#87CEEB
style C1 fill:#90EE90
graph TB
subgraph "scases 数组内容 (长度=3)"
A["scases[0]<br/>case ch1 <- 42"]
B["scases[1]<br/>case ch2 <- 'hello'"]
C["scases[2]<br/>case val := <-ch3"]
A1["c: *hchan(ch1)<br/>elem: *int(42的地址)"]
B1["c: *hchan(ch2)<br/>elem: *string('hello'的地址)"]
C1["c: *hchan(ch3)<br/>elem: *bool(val的地址)"]
A --> A1
B --> B1
C --> C1
end
style A fill:#FFB6C1
style B fill:#FFB6C1
style C fill:#87CEEB
scase 结构体详细内容:
| 索引 | 类型 | c (channel指针) | elem (数据指针) | 说明 |
|---|---|---|---|---|
| 0 | 发送 | &ch1 | &42 | 发送 42 到 ch1 |
| 1 | 发送 | &ch2 | &"hello" | 发送 "hello" 到 ch2 |
| 2 | 接收 | &ch3 | &val | 从 ch3 接收到 val |
order1、pollorder 和 lockorder 数组
order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0))
pollorder := order1[:ncases:ncases] // 前 3 个元素,轮询顺序
lockorder := order1[ncases:][:ncases:ncases] // 后 3 个元素,锁定顺序
内存布局图:
graph LR
subgraph "order1 数组布局 (长度=6)"
A["order1[0]<br/>pollorder[0]"]
B["order1[1]<br/>pollorder[1]"]
C["order1[2]<br/>pollorder[2]"]
D["order1[3]<br/>lockorder[0]"]
E["order1[4]<br/>lockorder[1]"]
F["order1[5]<br/>lockorder[2]"]
end
subgraph "用途说明"
G["轮询顺序<br/>(随机化)"]
H["锁定顺序<br/>(按地址排序)"]
end
A --> G
B --> G
C --> G
D --> H
E --> H
F --> H
style G fill:#90EE90
style H fill:#FFB6C1
Default Case 的特殊处理
关键问题:default case 存储在哪里?
答案是:default case 不存储在任何数据结构中,它通过 block 参数的值来标识其存在。
graph TB
subgraph "编译器对 Select 的处理"
A[分析 Select 语句]
B{是否有 default?}
C[block = false]
D[block = true]
E[只处理 channel case]
F[生成 selectgo 调用]
A --> B
B -->|有 default| C
B -->|无 default| D
C --> E
D --> E
E --> F
end
subgraph "数据结构分布"
G["scases 数组<br/>只存储 channel case"]
H["block 参数<br/>标识 default 存在"]
I["编译器生成的分支代码<br/>default 的实际执行逻辑"]
F --> G
F --> H
F --> I
end
style H fill:#FF6B6B
style I fill:#87CEEB
具体的数据结构分布:
| 组件 | 存储内容 | Default Case |
|---|---|---|
scases[] | channel 相关的 case | ❌ 不存储 |
pollorder[] | channel case 的随机顺序 | ❌ 不涉及 |
lockorder[] | channel case 的锁定顺序 | ❌ 不涉及 |
block 参数 | 是否有 default | ✅ false 表示有 default |
| 编译器生成代码 | default 分支的执行逻辑 | ✅ 实际的 default 代码 |
关键要点总结:
- scases: 存储所有非 default 的 case,长度等于
nsends + nrecvs - pollorder: 随机化的轮询顺序,确保公平性,避免饥饿
- lockorder: 按 channel 地址排序的锁定顺序,避免死锁
- order1: 同时存储 pollorder 和 lockorder 的连续内存块
- default:
- ❌ 不存储在 scases 中
- ✅ 通过
block=false标识其存在 - ✅ 通过返回值
casi=-1来触发 - ✅ 实际代码由编译器直接生成在 switch 分支中
2.2.2 生成随机化轮询顺序
// 编译器会将静态只有 0 或 1 个 case 加上 default 的 select 重写为更简单的结构
// 这里出现这种小的 ncase 值的唯一方式是在大的 select 中大多数 channel 被设为 nil
// 通用代码正确处理这些情况,且足够少见,不值得优化
// generate permuted order
// 生成随机化顺序
norder := 0
for i := range scases {
cas := &scases[i]
// Omit cases without channels from the poll and lock orders.
// 从轮询和锁定顺序中忽略没有 channel 的 case
if cas.c == nil {
cas.elem = nil // allow GC,允许垃圾回收
continue
}
// Timer channel 特殊处理
if cas.c.timer != nil {
cas.c.timer.maybeRunChan()
}
// 使用 Fisher-Yates 洗牌算法的一个变种生成随机顺序
j := cheaprandn(uint32(norder + 1)) // 生成 [0, norder] 范围的随机数
pollorder[norder] = pollorder[j] // 将 j 位置的值移到 norder 位置
pollorder[j] = uint16(i) // 将当前索引 i 放到 j 位置
norder++
}
pollorder = pollorder[:norder]
lockorder = lockorder[:norder]
生成随机轮询顺序示例
假设随机化后的轮询顺序为:
// 随机化前的顺序:[0, 1, 2]
// 随机化后的顺序:[1, 2, 0] (示例)
pollorder[0] = 1 // 先检查 case 1 (ch2 <- "hello")
pollorder[1] = 2 // 再检查 case 2 (val := <-ch3)
pollorder[2] = 0 // 最后检查 case 0 (ch1 <- 42)
随机化的重要性:
- 公平性: 防止某些 case 总是被优先选择
- 避免饥饿: 确保所有 case 都有被选中的机会
- 性能: 减少锁竞争中的"护航效应"(convoy effect)
2.2.4 Timer Channel 的特殊优化
在生成轮询顺序时,select 对 timer channel 进行了特殊处理:
// Timer channel 特殊处理
if cas.c.timer != nil {
cas.c.timer.maybeRunChan()
}
为什么需要 maybeRunChan 检查?
虽然 Go 调度器有全局的定时器检查机制,但 maybeRunChan() 解决了调度器检查的时机问题:
调度器的定时器检查机制
Go 调度器在多个层次检查定时器:
graph TD
A[系统启动] --> B[sysmon 系统监控器]
B --> C[每 10ms 检查一次]
C --> D[timeSleepUntil 检查所有P的定时器]
D --> E{有到期定时器?}
E -->|有| F[启动新的M来处理]
E -->|无| G[继续监控]
F --> H[新M获取P]
H --> I[运行定时器处理逻辑]
I --> J[ts.check 检查定时器堆]
J --> K[运行到期定时器]
L[调度循环 schedule] --> M[检查当前P的定时器]
M --> N[ts.check 运行到期定时器]
N --> O[findrunnable 寻找可运行goroutine]
O --> P[检查其他P的定时器 checkTimers]
Q[netpoller网络轮询器] --> R[wakeTime 唤醒时间]
R --> S[在指定时间唤醒]
style B fill:#FF6B6B
style J fill:#90EE90
style N fill:#90EE90
style P fill:#87CEEB
maybeRunChan 与调度器检查的对比
sequenceDiagram
participant U as 用户代码
participant S as Select
participant T as Timer
participant Sch as 调度器
participant P as Processor
Note over U,P: 场景:定时器已到期但还未被调度器处理
U->>T: time.NewTimer(1ns) 创建定时器
Note over T: 定时器立即到期
T->>P: 添加到P的定时器堆
U->>S: select { case <-timer.C: ... }
S->>T: 检查 timer != nil
alt 没有 maybeRunChan
S->>S: 直接检查channel状态
Note over S: Channel为空,没有值
S->>S: 进入阻塞等待
Note over Sch: 10ms后调度器检查
Sch->>P: ts.check() 发现到期定时器
Sch->>T: 运行定时器,发送值到channel
Sch->>S: 唤醒select
Note over U: 延迟了10ms!
else 有 maybeRunChan
S->>T: maybeRunChan() 主动检查
T->>T: 发现已到期,立即运行
T->>T: 发送值到channel
S->>S: 发现channel就绪
S->>U: 立即返回结果
Note over U: 几乎无延迟!
end
maybeRunChan 的核心作用
// maybeRunChan 检查定时器是否需要运行以向其关联的通道发送值
func (t *timer) maybeRunChan() {
// 如果定时器在堆中,普通定时器代码负责在适当时发送
if t.astate.Load()&timerHeaped != 0 {
return
}
t.lock()
now := nanotime()
// 检查定时器是否应该已经触发但还没有被处理
if t.state&timerHeaped != 0 || t.when == 0 || t.when > now {
// 定时器在堆中,或根本不运行,或未触发
t.unlock()
return
}
// 定时器已到期,立即运行
systemstack(func() {
t.unlockAndRun(now)
})
}
这种设计体现了 Go 运行时的精心优化:既有全局的调度器保障(确保定时器最终会被处理),又有局部的即时优化(在关键时刻立即检查),两者结合提供了高效且可靠的定时器系统。
2.2.5 生成锁定顺序(堆排序)
// sort the cases by Hchan address to get the locking order.
// simple heap sort, to guarantee n log n time and constant stack footprint.
// 按 Hchan 地址对 case 进行排序以获得锁定顺序
// 简单的堆排序,保证 n log n 时间复杂度和常量栈空间
// 构建最大堆
for i := range lockorder {
j := i
// Start with the pollorder to permute cases on the same channel.
// 从 pollorder 开始,以便对同一 channel 上的 case 进行排列
c := scases[pollorder[i]].c
// 向上调整堆
for j > 0 && scases[lockorder[(j-1)/2]].c.sortkey() < c.sortkey() {
k := (j - 1) / 2
lockorder[j] = lockorder[k]
j = k
}
lockorder[j] = pollorder[i]
}
// 堆排序:依次取出最大元素
for i := len(lockorder) - 1; i >= 0; i-- {
o := lockorder[i]
c := scases[o].c
lockorder[i] = lockorder[0] // 将最大元素移到末尾
j := 0
// 向下调整堆
for {
k := j*2 + 1
if k >= i {
break
}
if k+1 < i && scases[lockorder[k]].c.sortkey() < scases[lockorder[k+1]].c.sortkey() {
k++
}
if c.sortkey() < scases[lockorder[k]].c.sortkey() {
lockorder[j] = lockorder[k]
j = k
continue
}
break
}
lockorder[j] = o
}
锁定顺序的重要性:
- 避免死锁: 总是按相同顺序获取锁
- 内存地址排序: 使用 channel 的内存地址作为排序键
- 堆排序: O(n log n) 时间复杂度,O(1) 空间复杂度
graph TB
subgraph "锁定顺序生成过程"
A[随机轮询顺序 pollorder] --> B[提取 channel 地址]
B --> C[堆排序算法]
C --> D[按地址排序的 lockorder]
E[避免死锁] --> F[全局一致的锁定顺序]
F --> G[内存地址作为排序键]
D --> E
end
style C fill:#87CEEB
style E fill:#FF6B6B
style G fill:#90EE90
2.3 Select 与 Channel 的协调工作
2.3.1 Channel 锁定管理
// 锁定所有相关的 channel(来自 select.go)
func sellock(scases []scase, lockorder []uint16) {
var c *hchan
for _, o := range lockorder {
c0 := scases[o].c
if c0 != c { // 避免重复锁定同一个 channel
c = c0
lock(&c.lock) // 锁定 channel 的互斥锁
}
}
}
// 解锁所有相关的 channel(来自 select.go)
func selunlock(scases []scase, lockorder []uint16) {
// We must be very careful here to not touch sel after we have unlocked
// the last lock, because sel can be freed right after the last unlock.
// 我们在这里必须非常小心,不要在解锁最后一个锁后接触 sel,
// 因为 sel 可能在最后一个解锁后立即被释放
for i := len(lockorder) - 1; i >= 0; i-- {
c := scases[lockorder[i]].c
if i > 0 && c == scases[lockorder[i-1]].c {
continue // will unlock it on the next iteration
}
unlock(&c.lock) // 解锁 channel 的互斥锁
}
}
为什么需要重复锁定检查?
场景 1:sellock 中的重复锁定检查
if c0 != c { // 避免重复锁定同一个 channel
原因:同一个 channel 可能出现在多个 case 中
// 示例:同一个 channel 的多个操作
select {
case ch <- 1: // case 0: 发送操作
println("sent 1")
case ch <- 2: // case 1: 发送操作 (同一个 channel!)
println("sent 2")
case val := <-ch: // case 2: 接收操作 (同一个 channel!)
println("received:", val)
}
数据结构分析:
// scases 数组内容:
scases[0] = {c: &ch, elem: &1} // 发送 1
scases[1] = {c: &ch, elem: &2} // 发送 2
scases[2] = {c: &ch, elem: &val} // 接收
// lockorder 可能是:[0, 1, 2] (都指向同一个 channel)
// 如果没有重复检查,会对同一个 channel 锁定 3 次!
场景 2:selunlock 中的重复解锁检查
if i > 0 && c == scases[lockorder[i-1]].c {
continue // will unlock it on the next iteration
}
原因:确保每个 channel 只解锁一次,且在最后一个相关 case 处解锁
第一轮扫描与 Channel 状态检查:
graph TD
A[开始第一轮扫描] --> B{检查 case 类型}
B -->|接收操作| C[检查发送等待队列]
C --> C1{有等待的发送者?}
C1 -->|是| D[直接接收 goto recv]
C1 -->|否| C2[检查缓冲区]
C2 --> C3{缓冲区有数据?}
C3 -->|是| E[从缓冲区接收 goto bufrecv]
C3 -->|否| C4[检查关闭状态]
C4 --> C5{channel 已关闭?}
C5 -->|是| F[处理关闭 goto rclose]
C5 -->|否| G[继续下一个 case]
B -->|发送操作| H[检查关闭状态]
H --> H1{channel 已关闭?}
H1 -->|是| I[panic goto sclose]
H1 -->|否| H2[检查接收等待队列]
H2 --> H3{有等待的接收者?}
H3 -->|是| J[直接发送 goto send]
H3 -->|否| H4[检查缓冲区空间]
H4 --> H5{缓冲区有空间?}
H5 -->|是| K[发送到缓冲区 goto bufsend]
H5 -->|否| G
style D fill:#90EE90
style E fill:#87CEEB
style F fill:#FFB6C1
style I fill:#FF6B6B
style J fill:#90EE90
style K fill:#87CEEB
2.3.3 Channel 操作的具体实现
从缓冲区接收 (bufrecv)
bufrecv:
// can receive from buffer
// 可以从缓冲区接收
if raceenabled {
if cas.elem != nil {
raceWriteObjectPC(c.elemtype, cas.elem, casePC(casi), chanrecvpc)
}
racenotify(c, c.recvx, nil)
}
if msanenabled && cas.elem != nil {
msanwrite(cas.elem, c.elemtype.Size_)
}
if asanenabled && cas.elem != nil {
asanwrite(cas.elem, c.elemtype.Size_)
}
recvOK = true
qp = chanbuf(c, c.recvx) // 获取缓冲区接收位置
if cas.elem != nil {
typedmemmove(c.elemtype, cas.elem, qp) // 复制数据到接收目标
}
typedmemclr(c.elemtype, qp) // 清零缓冲区槽位
c.recvx++ // 移动接收索引
if c.recvx == c.dataqsiz {
c.recvx = 0 // 环形缓冲区回到开头
}
c.qcount-- // 减少缓冲区元素数量
selunlock(scases, lockorder) // 解锁所有 channel
goto retc
与 Channel 内部逻辑的关系:
- 使用
chanbuf(c, c.recvx)获取缓冲区位置(来自 chan.go) - 更新
c.recvx和c.qcount,与 channel 的环形缓冲区机制一致 - 调用
typedmemmove进行类型安全的内存复制
发送到缓冲区 (bufsend)
bufsend:
// can send to buffer
// 可以发送到缓冲区
if raceenabled {
racenotify(c, c.sendx, nil)
raceReadObjectPC(c.elemtype, cas.elem, casePC(casi), chansendpc)
}
if msanenabled {
msanread(cas.elem, c.elemtype.Size_)
}
if asanenabled {
asanread(cas.elem, c.elemtype.Size_)
}
typedmemmove(c.elemtype, chanbuf(c, c.sendx), cas.elem) // 复制数据到缓冲区
c.sendx++ // 移动发送索引
if c.sendx == c.dataqsiz {
c.sendx = 0 // 环形缓冲区回到开头
}
c.qcount++ // 增加缓冲区元素数量
selunlock(scases, lockorder) // 解锁所有 channel
goto retc
直接传输操作
recv:
// can receive from sleeping sender (sg)
// 可以从休眠的发送者接收
recv(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
if debugSelect {
print("syncrecv: cas0=", cas0, " c=", c, "\n")
}
recvOK = true
goto retc
send:
// can send to a sleeping receiver (sg)
// 可以发送给休眠的接收者
if raceenabled {
raceReadObjectPC(c.elemtype, cas.elem, casePC(casi), chansendpc)
}
if msanenabled {
msanread(cas.elem, c.elemtype.Size_)
}
if asanenabled {
asanread(cas.elem, c.elemtype.Size_)
}
send(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
if debugSelect {
print("syncsend: cas0=", cas0, " c=", c, "\n")
}
goto retc
2.4 Select 与 Channel 的协调工作流程图
场景 1:Select 发送操作的协调流程
sequenceDiagram
participant S as Select
participant C as Channel
participant G1 as Sender Goroutine
participant G2 as Receiver Goroutine
Note over S,G2: Select 发送操作协调流程
S->>C: sellock(按 lockorder 锁定 channel)
S->>C: 检查 recvq 接收等待队列
alt 有等待的接收者
C->>S: dequeue() 返回等待的 sudog
S->>C: send(直接传输数据到接收者)
C->>G2: sendDirect(复制数据到接收者栈)
S->>C: unlockf(解锁 channel)
S->>G2: goready(唤醒接收者 goroutine)
Note over G2: 接收者被调度执行
else 缓冲区有空间
S->>C: chanbuf(获取缓冲区发送位置)
S->>C: typedmemmove(复制数据到缓冲区)
S->>C: 更新 sendx, qcount++
S->>C: selunlock(解锁所有 channel)
Note over S: 发送操作完成,返回
else 需要阻塞等待
S->>C: acquireSudog(获取等待结构体)
S->>C: sendq.enqueue(加入发送等待队列)
S->>S: gopark(阻塞当前 goroutine)
Note over S: goroutine 进入睡眠状态
Note over G2,C: 稍后接收者到达
G2->>C: recv 操作检查 sendq
C->>G2: dequeue() 获取等待的发送者
C->>S: 复制数据并唤醒发送者
S->>C: 从等待队列中清理
Note over S: 发送者被唤醒并完成操作
end
场景 2:Select 接收操作的协调流程
sequenceDiagram
participant S as Select
participant C as Channel
participant G1 as Sender Goroutine
participant G2 as Receiver Goroutine
Note over S,G1: Select 接收操作协调流程
S->>C: sellock(按 lockorder 锁定 channel)
S->>C: 检查 sendq 发送等待队列
alt 有等待的发送者
C->>S: dequeue() 返回等待的 sudog
S->>C: recv(从发送者直接接收数据)
C->>G1: 复制发送者数据到接收目标
S->>C: unlockf(解锁 channel)
S->>G1: goready(唤醒发送者 goroutine)
Note over G1: 发送者被调度执行
else 缓冲区有数据
S->>C: chanbuf(获取缓冲区接收位置)
S->>C: typedmemmove(从缓冲区复制数据)
S->>C: 更新 recvx, qcount--
S->>C: selunlock(解锁所有 channel)
Note over S: 接收操作完成,返回数据
else Channel 已关闭
S->>C: 检测到 c.closed != 0
S->>C: selunlock(解锁所有 channel)
S->>S: typedmemclr(清零接收目标)
Note over S: 返回零值和 false
else 需要阻塞等待
S->>C: acquireSudog(获取等待结构体)
S->>C: recvq.enqueue(加入接收等待队列)
S->>S: gopark(阻塞当前 goroutine)
Note over S: goroutine 进入睡眠状态
Note over G1,C: 稍后发送者到达
G1->>C: send 操作检查 recvq
C->>G1: dequeue() 获取等待的接收者
C->>S: 复制数据并唤醒接收者
S->>C: 从等待队列中清理
Note over S: 接收者被唤醒并完成操作
end
2.5 第二轮:注册等待操作
当第一轮扫描没有找到就绪操作且处于阻塞模式时,select 需要在相关 channel 上注册等待:
if !block {
// 非阻塞模式(有 default 分支)
selunlock(scases, lockorder) // 解锁所有 channel
casi = -1 // 返回 -1 表示选择了 default
goto retc
}
// pass 2 - enqueue on all chans
// 第二轮 - 在所有 channel 上入队等待
gp = getg()
if gp.waiting != nil {
throw("gp.waiting != nil")
}
nextp = &gp.waiting
for _, casei := range lockorder {
casi = int(casei)
cas = &scases[casi]
c = cas.c
sg := acquireSudog() // 获取 sudog 结构体
sg.g = gp // 设置 goroutine
sg.isSelect = true // 标记为 select 操作
// No stack splits between assigning elem and enqueuing
// sg on gp.waiting where copystack can find it.
// 在赋值 elem 和将 sg 加入 gp.waiting 之间不能有栈分裂
// 因为 copystack 可能会找到它
sg.elem = cas.elem
sg.releasetime = 0
if t0 != 0 {
sg.releasetime = -1
}
sg.c = c
// Construct waiting list in lock order.
// 按锁定顺序构建等待列表
*nextp = sg
nextp = &sg.waitlink
if casi < nsends {
c.sendq.enqueue(sg) // 发送操作:加入发送等待队列
} else {
c.recvq.enqueue(sg) // 接收操作:加入接收等待队列
}
// Timer channel 特殊处理
if c.timer != nil {
blockTimerChan(c)
}
}
// 等待某人唤醒我们
gp.param = nil
// 通知任何试图收缩我们栈的人,我们即将在 channel 上阻塞
// 从这个 G 的状态改变到我们设置 gp.activeStackChans 之间的窗口
// 对栈收缩来说是不安全的
gp.parkingOnChan.Store(true)
gopark(selparkcommit, nil, waitReasonSelect, traceBlockSelect, 1)
gp.activeStackChans = false
blockTimerChan(c) 是 Go 运行时中用于Timer Channel 生命周期管理的关键机制。它的核心作用是:当 goroutine 决定在一个 timer channel 上阻塞时,确保该 timer 被正确添加到定时器堆中进行管理。
2.6 特殊的 selparkcommit 函数
// selparkcommit 是 select 专用的 park 提交函数
func selparkcommit(gp *g, _ unsafe.Pointer) bool {
// There are unlocked sudogs that point into gp's stack. Stack
// copying must lock the channels of those sudogs.
// Set activeStackChans here instead of before we try parking
// because we could self-deadlock in stack growth on a
// channel lock.
// 有未锁定的 sudog 指向 gp 的栈。栈复制必须锁定那些 sudog 的 channel
// 在这里设置 activeStackChans 而不是在我们尝试阻塞之前设置
// 因为我们可能在 channel 锁上的栈增长中自死锁
gp.activeStackChans = true
// Mark that it's safe for stack shrinking to occur now,
// because any thread acquiring this G's stack for shrinking
// is guaranteed to observe activeStackChans after this store.
// 标记现在栈收缩是安全的,
// 因为任何为了收缩而获取这个 G 的栈的线程
// 都保证在这个存储后观察到 activeStackChans
gp.parkingOnChan.Store(false)
// Make sure we unlock after setting activeStackChans and
// unsetting parkingOnChan. The moment we unlock any of the
// channel locks we risk gp getting readied by a channel operation
// and so gp could continue running before everything before the
// unlock is visible (even to gp itself).
// 确保我们在设置 activeStackChans 和取消设置 parkingOnChan 后解锁
// 我们解锁任何 channel 锁的那一刻就有 gp 被 channel 操作准备好的风险
// 所以 gp 可能在解锁前的所有东西都可见之前继续运行(甚至对 gp 本身)
// This must not access gp's stack (see gopark). In
// particular, it must not access the *hselect. That's okay,
// because by the time this is called, gp.waiting has all
// channels in lock order.
// 这不能访问 gp 的栈(见 gopark)
// 特别是,它不能访问 *hselect。这没关系,
// 因为当这被调用时,gp.waiting 有所有按锁定顺序的 channel
var lastc *hchan
for sg := gp.waiting; sg != nil; sg = sg.waitlink {
if sg.c != lastc && lastc != nil {
// As soon as we unlock the channel, fields in
// any sudog with that channel may change,
// including c and waitlink. Since multiple
// sudogs may have the same channel, we unlock
// only after we've passed the last instance
// of a channel.
// 一旦我们解锁 channel,任何带有该 channel 的 sudog 中的字段
// 可能会改变,包括 c 和 waitlink。由于多个 sudog 可能有相同的 channel,
// 我们只在通过了 channel 的最后一个实例后才解锁
unlock(&lastc.lock)
}
lastc = sg.c
}
if lastc != nil {
unlock(&lastc.lock)
}
return true
}
2.7 第三轮:唤醒后的清理和结果获取
// 被唤醒后重新锁定
sellock(scases, lockorder)
gp.selectDone.Store(0)
sg = (*sudog)(gp.param) // 获取唤醒的 sudog
gp.param = nil
// pass 3 - dequeue from unsuccessful chans
// otherwise they stack up on quiet channels
// record the successful case, if any.
// We singly-linked up the SudoGs in lock order.
// 第三轮 - 从未成功的 channel 中出队
// 否则它们会在安静的 channel 上堆积
// 记录成功的 case(如果有的话)
// 我们按锁定顺序单链接了 SudoG
casi = -1
cas = nil
caseSuccess = false
sglist = gp.waiting
// Clear all elem before unlinking from gp.waiting.
// 在从 gp.waiting 断开链接之前清除所有 elem
for sg1 := gp.waiting; sg1 != nil; sg1 = sg1.waitlink {
sg1.isSelect = false
sg1.elem = nil
sg1.c = nil
}
gp.waiting = nil
for _, casei := range lockorder {
k = &scases[casei]
if k.c.timer != nil {
unblockTimerChan(k.c)
}
if sg == sglist {
// sg has already been dequeued by the G that woke us up.
// sg 已经被唤醒我们的 G 出队了
casi = int(casei)
cas = k
caseSuccess = sglist.success
if sglist.releasetime > 0 {
caseReleaseTime = sglist.releasetime
}
} else {
c = k.c
if int(casei) < nsends {
c.sendq.dequeueSudoG(sglist) // 从发送队列移除
} else {
c.recvq.dequeueSudoG(sglist) // 从接收队列移除
}
}
sgnext = sglist.waitlink
sglist.waitlink = nil
releaseSudog(sglist) // 释放 sudog
sglist = sgnext
}
if cas == nil {
throw("selectgo: bad wakeup")
}
c = cas.c
if debugSelect {
print("wait-return: cas0=", cas0, " c=", c, " cas=", cas, " send=", casi < nsends, "\n")
}
if casi < nsends {
if !caseSuccess {
goto sclose // 发送失败,channel 可能已关闭
}
} else {
recvOK = caseSuccess // 接收操作的成功标志
}
与 Channel dequeueSudoG 的关系:
// 来自 select.go 的特殊出队函数
func (q *waitq) dequeueSudoG(sgp *sudog) {
x := sgp.prev
y := sgp.next
if x != nil {
if y != nil {
// middle of queue - 队列中间
x.next = y
y.prev = x
sgp.next = nil
sgp.prev = nil
return
}
// end of queue - 队列末尾
x.next = nil
q.last = x
sgp.prev = nil
return
}
if y != nil {
// start of queue - 队列开头
y.prev = nil
q.first = y
sgp.next = nil
return
}
// x==y==nil. Either sgp is the only element in the queue,
// or it has already been removed. Use q.first to disambiguate.
// x==y==nil。要么 sgp 是队列中的唯一元素,
// 要么它已经被移除了。使用 q.first 来消除歧义。
if q.first == sgp {
q.first = nil
q.last = nil
}
}
3. Select 完整执行流程时序图
3.1 整体执行流程概览
flowchart TD
A["用户代码执行 select"] --> B["编译器生成 selectgo 调用"]
B --> C["selectgo 初始化"]
C --> D["过滤 nil channel"]
D --> E["生成随机轮询顺序"]
E --> F["生成锁定顺序"]
F --> G["锁定所有 channel"]
G --> H["第一轮扫描:查找就绪操作"]
H --> I{"找到就绪操作?"}
I -->|是| J["执行就绪操作"]
I -->|否| K{"是否有 default?"}
K -->|是| L["解锁并返回 default"]
K -->|否| M["第二轮:注册等待"]
M --> N["在所有 channel 上入队"]
N --> O["gopark 阻塞等待"]
O --> P["被其他 goroutine 唤醒"]
P --> Q["第三轮:清理和获取结果"]
J --> R["返回结果给用户"]
L --> R
Q --> R
style I fill:#87CEEB
style K fill:#FFB6C1
style O fill:#FF6B6B
style R fill:#90EE90
3.2 详细的分阶段时序图
3.2.1 阶段一:初始化和准备阶段
sequenceDiagram
participant U as 用户代码
participant C as 编译器
participant S as selectgo
participant M as 内存管理
participant R as 随机数生成器
Note over U,R: 阶段一:初始化和准备
U->>C: select 语句
C->>C: 分析 case 数量和类型
C->>C: 生成 scase 数组和 order 数组
C->>S: selectgo(cas0, order0, pc0, nsends, nrecvs, block)
S->>S: 参数验证和调试输出
S->>M: 转换指针类型 cas1, order1
S->>S: 创建切片 scases, pollorder, lockorder
S->>S: 初始化 race detector 和性能分析
Note over S: 过滤 nil channel 和 timer 检查
loop 遍历所有 case
S->>S: 检查 cas.c == nil
alt channel 不为 nil
S->>S: timer 特殊处理 maybeRunChan()
S->>R: cheaprandn() 生成随机数
S->>S: Fisher-Yates 洗牌更新 pollorder
else channel 为 nil
S->>S: 跳过并标记 GC
end
end
S->>S: 堆排序生成 lockorder
Note over S: 准备阶段完成
3.2.2 阶段二:第一轮扫描 - 查找就绪操作
sequenceDiagram
participant S as selectgo
participant Ch1 as Channel1
participant Ch2 as Channel2
participant Ch3 as Channel3
participant G1 as Goroutine1
participant G2 as Goroutine2
Note over S,G2: 阶段二:第一轮扫描
S->>Ch1: sellock 锁定所有channel
S->>Ch2: lock ch2
S->>Ch3: lock ch3
Note over S: 按pollorder顺序检查
loop 遍历pollorder
alt 接收操作
S->>Ch1: 检查sendq队列
Ch1->>S: 返回等待的发送者
alt 有等待发送者
S->>G1: 准备直接传输
Note over S: goto recv
else 无等待发送者
S->>Ch1: 检查缓冲区
alt 缓冲区有数据
Note over S: goto bufrecv
else 缓冲区为空
S->>Ch1: 检查关闭状态
alt 已关闭
Note over S: goto rclose
else 未关闭
Note over S: 继续下一个case
end
end
end
else 发送操作
S->>Ch2: 检查关闭状态
alt 已关闭
Note over S: goto sclose
else 未关闭
S->>Ch2: 检查recvq队列
Ch2->>S: 返回等待的接收者
alt 有等待接收者
S->>G2: 准备直接传输
Note over S: goto send
else 无等待接收者
S->>Ch2: 检查缓冲区空间
alt 有空间
Note over S: goto bufsend
else 无空间
Note over S: 继续下一个case
end
end
end
end
end
Note over S: 未找到就绪操作
3.2.3 阶段三:执行就绪操作或处理非阻塞情况
sequenceDiagram
participant S as selectgo
participant Ch as Channel
participant G as Goroutine
participant U as UserCode
Note over S,U: 阶段三:执行就绪操作
alt 直接传输
S->>Ch: send或recv直接传输
Ch->>G: sendDirect复制数据
S->>Ch: unlockf解锁
S->>G: goready唤醒goroutine
S->>S: 更新统计信息
S->>U: 返回结果
else 缓冲区操作
S->>Ch: chanbuf获取缓冲区位置
S->>Ch: typedmemmove复制数据
S->>Ch: 更新sendx recvx qcount
S->>Ch: selunlock解锁所有channel
S->>S: 更新统计信息
S->>U: 返回结果
else 关闭的channel
S->>Ch: selunlock解锁所有channel
S->>S: typedmemclr清零接收目标
S->>U: 返回结果或panic
else 有default
S->>Ch: selunlock解锁所有channel
S->>S: casi设为-1
S->>U: 返回default
else 无default
Note over S: 进入第四阶段
end
3.2.4 阶段四:注册等待和阻塞
sequenceDiagram
participant S as selectgo
participant Ch1 as Channel1
participant Ch2 as Channel2
participant T as Timer
participant Sch as Scheduler
participant GP as Goroutine
Note over S,GP: 阶段四:注册等待和阻塞
S->>GP: getg获取当前goroutine
S->>S: 初始化gp.waiting链表
Note over S: 按lockorder顺序注册等待
loop 遍历lockorder
S->>S: acquireSudog获取等待结构体
S->>S: 设置sg.g sg.isSelect sg.elem
S->>S: 构建gp.waiting链表
alt 发送操作
S->>Ch1: sendq.enqueue加入发送队列
else 接收操作
S->>Ch2: recvq.enqueue加入接收队列
end
alt timer channel
S->>T: blockTimerChan特殊处理
T->>T: blocked计数增加
T->>T: 检查是否需要加入定时器堆
end
end
S->>GP: 清空gp.param
S->>GP: parkingOnChan设为true
S->>Sch: gopark阻塞当前goroutine
Note over Sch: selparkcommit执行
S->>GP: activeStackChans设为true
S->>GP: parkingOnChan设为false
S->>Ch1: unlock按顺序解锁所有channel
S->>Ch2: unlock
Note over GP: Goroutine进入睡眠状态
3.2.5 阶段五:被唤醒和清理
sequenceDiagram
participant Other as OtherGoroutine
participant Ch as Channel
participant S as selectgo
participant T as Timer
participant GP as Goroutine
participant U as UserCode
Note over Other,U: 阶段五:被唤醒清理和返回结果
Note over Other,Ch: 其他goroutine操作channel唤醒当前goroutine
Other->>Ch: send或recv操作
Ch->>Ch: dequeue获取等待的sudog
Ch->>GP: goready唤醒goroutine
GP->>S: 从gopark返回继续执行
Note over S: 第三轮清理和获取结果
S->>Ch: sellock重新锁定所有channel
S->>GP: selectDone重置选择标志
S->>GP: 获取唤醒的sudog
S->>GP: 清空gp.param
S->>S: 初始化清理变量
S->>GP: 获取gp.waiting等待链表
Note over S: 清理所有sudog引用避免内存泄漏
loop 清理gp.waiting链表
S->>S: sg1.isSelect设为false
S->>S: sg1.elem设为nil
S->>S: sg1.c设为nil
end
S->>GP: gp.waiting设为nil
Note over S: 从未成功的channel中移除等待
loop 按lockorder遍历
alt timer channel
S->>T: unblockTimerChan减少阻塞计数
T->>T: blocked减1
alt blocked等于0
T->>T: 标记为僵尸延迟清理
end
end
alt 当前sglist是被唤醒的sudog
S->>S: 记录成功的case信息
S->>S: 设置casi和caseSuccess
else 当前sglist不是被唤醒的
alt 发送操作
S->>Ch: 从发送队列移除sglist
else 接收操作
S->>Ch: 从接收队列移除sglist
end
end
S->>S: releaseSudog释放sudog到池中
end
S->>Ch: selunlock解锁所有channel
Note over S: 处理特殊情况和最终结果
alt 发送操作且不成功
Note over S: goto sclose channel被关闭
else 接收操作
S->>S: recvOK设置接收成功标志
end
S->>S: 更新性能分析统计
S->>U: 返回最终结果
通过这个深入的源码分析,我们可以看到 Go 语言的 select 机制与 channel 是如何精密协作的:
- 锁定协调: Select 通过地址排序确保一致的锁定顺序,避免死锁
- 等待队列管理: Select 使用 channel 的等待队列机制,但增加了特殊的竞争处理
- 内存模型: Select 遵循 channel 的内存模型,确保数据传输的正确性
- 性能优化: 两者协作实现了高效的多路复用和公平性保证
这种设计使得 Go 的并发编程既安全又高效,是现代编程语言并发设计的典范。