kotlinx.coroutines select表达式:同时监听多个协程结果的终极方案

原创2026-09-21 18:33:361,614 阅读
文章标签:异步编程并发编程跨平台

kotlinx.coroutines select表达式:同时监听多个协程结果的终极方案

在 Kotlin 开发中,我们经常需要同时监听多个协程的结果:等哪个先完成就用哪个,或者哪个通道先有数据就处理哪个。Kotlin 官方协程库 kotlinx.coroutines 为此提供了强大的 select 表达式——它能让一个协程"并发等待"多个挂起函数,并原子地选中第一个可用的结果,避免轮询、避免忙等。本文面向新手,用最少代码讲清 select 表达式的用法、核心条款与实战技巧。

kotlinx.coroutines 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'") }  // 第二个子句
}

kotlinx.coroutines 多个协程通过通道同时发送与接收数据

运行七次会看到 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

多个协程并发执行并挂起等待,select表达式同时监听其结果

这个模式非常适合"多路请求竞速取最快响应"的场景,完整示例见 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 在响应式场景中的利器。

登录后查看全文
kotlinx.coroutines