Kotlin produceIn 的作用域取消后,receive 为什么有时抛 JobCancellationException
接收协程没被取消,receive 却收到 JobCancellationException 时,检查生产作用域和 channel 的结束原因。区分正常关闭、生产取消与接收者取消,用 receiveCatching 明确处理。
测试期待 ClosedReceiveChannelException,却偶尔得到 JobCancellationException;接收者所在的协程明明还没取消。若 channel 来自 flow.produceIn(otherScope),应分开检查生产者正常结束与生产者作用域取消。
这类现象在 kotlinx.coroutines 1.10.2 有具体表现,属于 channel 的关闭与取消语义,不应直接写成需要升级才能修复的运行时回归。
接收者的 Job 活着,channel 仍可能被取消
produceIn(scope) 会在传入的作用域中启动生产协程,收集 flow。flow 正常完成时,channel 正常关闭;生产协程在完成前被取消,channel 则携带取消原因结束。
channel 已经没有剩余元素时,receive() 对正常关闭抛出 ClosedReceiveChannelException;若关闭带有原因,则把这个原因抛给接收者。所以收到 JobCancellationException,可以是生产端结束原因传到了接收端,不足以证明接收协程自身的 Job 已被取消。
还有一种独立情况:接收者本身在挂起等待中被取消。receive() 有及时取消保证,也会抛 CancellationException。诊断时把 channel 的来源、生产作用域、接收协程的 Job 及完整异常放在一起看,不能只靠异常类名区分是哪一端先结束。
空 flow 也存在调度顺序
例如创建 channel 后立即取消生产作用域:
val channel = source.produceIn(producerScope)
producerScope.cancel()
channel.receive()
片段需要已有的 source、producerScope 及对应协程导入。创建 channel 不等于生产协程已经完成:空 flow 可能先正常结束,也可能还没来得及运行就遭取消,因此测试只断言一种异常会依赖调度时序。
MutableSharedFlow 则不会自然完成;取消其生产作用域时,不应期待它先走完一条普通的完成路径。flow { }、emptyFlow() 与共享流在此处不能视为同一种生命周期。
用固定延时或多跑几次让测试“稳定”,并没有消除这层竞争。若需求就是验证正常完成,应让生产端确定完成后再检查空 channel;若需求是取消,便应按取消语义断言,而不是要求它伪装成正常关闭。
需要判断结束原因时,用 receiveCatching
下面示例遍历 ReceiveChannel<String>,把正常结束与带原因结束分开。它应放在挂起函数中,业务处理由 consume 提供:
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.channels.ReceiveChannel
suspend fun drain(
channel: ReceiveChannel<String>,
consume: (String) -> Unit
) {
while (true) {
val result = channel.receiveCatching()
if (result.isClosed) {
currentCoroutineContext().ensureActive()
val cause = result.exceptionOrNull()
if (cause != null) throw cause
break
}
consume(result.getOrThrow())
}
}
正常关闭且读完时退出;带原因结束时保留原原因。这段没有把生产取消自动当作成功。某些业务允许生产方主动停止,则可在明确业务规则后处理这个分支,不能统一吞掉所有 CancellationException。
receiveCatching() 也不是“绝不抛异常”:接收协程自身被取消时仍会抛 CancellationException。这里再次 ensureActive(),避免在处理 channel 结束结果时掩盖接收者自己的取消状态。
测试时分别覆盖有限 flow 正常完成、持续流的生产作用域取消、接收者自身取消,以及 channel 中尚有元素的情况。断言应对应每个场景的原因,不把调度先后造成的两种合法结果误判成不稳定库缺陷。


