子兮子兮

select 是 Go 并发中最强大的语法之一,同时也是生产事故的高发区之一。
很多并发问题并不是来自复杂算法,而是来自一些看起来完全合理的 select 写法。
这些问题往往:
这篇文章结合真实工程经验,分析三类最常见、最隐蔽的 select 使用问题。
select 的语义非常灵活:
但问题在于:
select 本身不管理生命周期、不保证公平,也不保证退出路径。
换句话说:
而生产事故,往往就发生在“没有收尾”的地方。
这是生产中最常见的一类问题。
func request() {
ch := make(chan Result)
go func() {
ch <- callRemote()
}()
select {
case res := <-ch:
handle(res)
case <-time.After(1 * time.Second):
return
}
}
看起来完全合理:
当 timeout 触发:
select 直接 returncallRemote 执行完毕ch但没有接收者了,于是:
goroutine 永远阻塞在发送操作上
每一次 timeout,都会留下一个无法退出的 goroutine。
ch := make(chan Result, 1)
避免发送阻塞。
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
go func(ctx context.Context) {
select {
case <-ctx.Done():
return
default:
result := callRemote()
ch <- result
}
}(ctx)
核心原则:
timeout 只是控制等待,不是控制 goroutine 生命周期。
这是另一个非常常见的事故源。
for {
select {
case msg := <-ch:
process(msg)
default:
}
}
很多人以为:
这个循环会变成无限忙循环。
CPU 使用率会迅速升高,因为:
default 立即执行结果:
如果你想:
msg := <-ch
ticker := time.NewTicker(time.Second)
select {
case msg := <-ch:
case <-ticker.C:
}
必须配合:
runtime.Gosched()
time.Sleep()
否则就是一个隐形的 CPU 炸弹。
很多人误以为 select 是公平调度。
实际上:
select是伪随机选择,并不保证公平。
select {
case fast := <-fastCh:
handleFast(fast)
case slow := <-slowCh:
handleSlow(slow)
}
如果:
fastCh 高频slowCh 低频那么:
fastCh 会持续被命中slowCh 可能长期得不到处理这叫 channel 饥饿。
不要把不同优先级放在同一个 select 中。
例如:
slowCh而不是依赖 select 的调度行为。
timeout ≠ goroutine 结束。
99% 的 default 都可以被:
tickercontext替代。
它:
复杂调度必须自己设计。
上线前可以自查:
default?channel 是否存在吞噬风险?select 是 Go 并发的核心工具之一,但它只提供了“选择”,没有提供“安全”。
生产环境中的并发稳定性,往往不是由复杂技术决定,而是由这些基础结构的细节决定。
如果你发现系统:
请第一时间检查 select。
| 内容声明 | |
|---|---|
| 标题: Go 并发稳定性(1):select 使用不当引发的三类并发问题 | |
| 链接: https://zixizixi.cn/go-concurrency-stability-1-select | 来源: iTanken |
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可,转载请保留此声明。
| |