kotlinx.coroutines select表达式:同时监听多个协程结果的终极方案
kotlinx.coroutines select表达式:同时监听多个协程结果的终极方案
在 Kotlin 开发中,我们经常需要同时监听多个协程的结果:等哪个先完成就用哪个,或者哪个通道先有数据就处理哪个。Kotlin 官方协程库 kotlinx.coroutines 为此提供了强大的 select 表达式——它能让一个协程"并发等待"多个挂起函数,并原子地选中第一个可用的结果,避免轮询、避免忙等。本文面向新手,用最少代码讲清 select 表达式的用法、核心条款与实战技巧。
一句话理解 select:协程世界的"竞速等待"
传统写法是依次 await() 多个 Deferred,只能等到全部完成;select 则相反——谁先完成,就立刻执行对应分支,其余分支自动失效。它像一个"竞速器":
suspend fun firstResult(a: Deferred<String>, b: Deferred<String>): String =
select {
a.onAwait { "来自 a: $it" } // 第一个子句:a 先完成则执行
b.onAwait { "来自 b: $it" } // 第二个子句:b 先完成则执行
}
select 是一个 Kotlin DSL:在 lambda 内写多少条款,就同时监听多少件事。官方定义见 Select.kt,完整教程见 select-expression.md。
select 表达式支持哪些监听条款(速查表)
| 监听对象 | 挂起函数 | select 条款 | 典型场景 |
|---|---|---|---|
| 通道接收 | receive |
onReceive |
谁先有数据收谁 |
| 通道接收 | receiveCatching |
onReceiveCatching |
同时处理"有数据/已关闭" |
| 通道发送 | send |
onSend |
主通道繁忙时改发副通道 |
| Deferred 结果 | await |
onAwait |
多个协程竞速取结果 |
| Job 结束 | join |
onJoin |
等待任务完成 |
| 超时 | delay |
onTimeout |
给 select 加兜底超时 |
这张表来自 Select.kt 中的官方文档注释,是编写 select 时的第一参考。
实战 1:用 onReceive 同时监听两个通道
假设两个生产者协程 fizz 每 500ms 发一条消息,buzz 每 1000ms 发一条。用 receive 只能一个个排队接收,而 select 让两个通道"同台竞技":
select<Unit> { // Unit 表示 select 本身不产生结果
fizz.onReceive { println("fizz -> '$it'") } // 第一个子句
buzz.onReceive { println("buzz -> '$it'") } // 第二个子句
}
运行七次会看到 fizz 与 buzz 的交替输出(fizz 快一倍,出现次数也大约多一倍),这正是"谁先就绪选谁"的效果。完整可运行示例见 example-select-01.kt。
⚠️ 注意:onReceive 遇到已关闭的通道会抛异常。如需把"通道关闭"也当作一种可处理的结果,请改用 onReceiveCatching 条款,见 example-select-02.kt。
实战 2:用 onAwait 竞速等待 12 个协程结果
select 是 DSL,条款可以用任意代码动态生成。下面对一批 Deferred 循环注册 onAwait,谁先算完谁赢:
val result = select<String> {
list.withIndex().forEach { (index, deferred) ->
deferred.onAwait { "Deferred $index -> '$it'" }
}
}
输出示例:
Deferred 4 produced answer 'Waited for 128 ms'
11 coroutines are still active
这个模式非常适合"多路请求竞速取最快响应"的场景,完整示例见 example-select-04.kt。
超时兜底与"偏向"特性:两个必须知道的行为
1️⃣ onTimeout 给 select 加保险。 在条款中写入超时,若其他子句迟迟不就绪,超时子句会被选中:
select<String> {
a.onAwait { "a 先完成" }
onTimeout(1000) { "超时了,1 秒内没人完成" }
}
实现在 OnTimeout.kt。
2️⃣ select 是"有偏向"的。 当多个条款同时就绪时,第一个子句优先。这既是特性也是坑:想优先走某分支就把它放前面;想要公平随机,请使用 selectUnbiased。偏向行为有专门测试覆盖,见 SelectBiasTest.kt。
whileSelect:select 的循环快捷键
需要"有数据就处理、否则阻塞等待"的循环场景,whileSelect 是 while(select { ... }) 的语法糖,条款返回 true 就继续循环:
whileSelect {
channelA.onReceive { handleA(it); true }
channelB.onReceive { handleB(it); true }
}
源码仅 6 行,非常直观,见 WhileSelect.kt。
新手常见误区清单
- ❌ 以为 select 会并发执行多个条款——最多只有一个条款被原子选中,其余自动失效。
- ❌ 用 onReceive 监听可能关闭的通道——关闭即抛异常,应改用
onReceiveCatching。 - ❌ 忽略子句顺序——多路同时就绪时第一个子句赢,顺序即优先级。
- ❌ 在 select 中写耗时同步代码——条款块应保持轻量,重活交给其他协程。
- ⚠️ select 是实验性 API,官方提示未来版本可能调整,生产使用前建议关注版本变更说明。
延伸阅读与源码导航 📚
| 资源 | 路径 |
|---|---|
| 官方 select 教程 | docs/topics/select-expression.md |
| select 核心实现 | kotlinx-coroutines-core/common/src/selects/Select.kt |
| 无偏向版本 | kotlinx-coroutines-core/common/src/selects/SelectUnbiased.kt |
| 超时条款 | kotlinx-coroutines-core/common/src/selects/OnTimeout.kt |
| 可运行示例 1~5 | kotlinx-coroutines-core/jvm/test/guide/(example-select-01 ~ 05) |
| 行为测试(偏向/超时/通道) | kotlinx-coroutines-core/common/test/selects/ |
掌握 select 后,你就能优雅地处理"多个协程结果谁先要谁"的并发难题——这正是 kotlinx.coroutines 在响应式场景中的利器。


