Kotlin协程:含yield()的无限循环无法取消问题咨询
问题分析:为什么yield()的无限循环在runTest中无法被取消
核心原因:TestDispatcher的即时执行特性与yield()的行为冲突
你遇到的问题本质是runTest使用的TestDispatcher调度器的特性,和yield()在该调度器下的行为导致的,具体拆解如下:
1. runTest的调度器特性
runTest默认使用的TestDispatcher是单线程、即时执行的调度器:
- 所有协程任务会尽可能在当前线程立即执行,不会真正挂起等待真实时间
- 只有遇到
delay()这类明确需要时间推进的挂起函数时,调度器才会模拟时间流逝,切换执行其他等待中的任务
2. 第一个测试用例的执行流程
当你用yield()时,协程的执行逻辑变成了:
backgroundScope.launch { while(isActive) { yield() // 在TestDispatcher中,yield仅将当前任务放到队列末尾 } }
这个协程会被runTest立即启动并执行:
- 进入
while(isActive)循环,第一次调用yield():调度器把当前循环任务放到任务队列末尾,但此时队列里没有其他等待执行的任务(delay(1000)还没开始执行) - 调度器立即重新取出队列末尾的循环任务,再次进入循环调用
yield(),形成无限循环占用线程的局面 - 主线程被这个循环完全占用,导致后续的
delay(1000)和job.cancelAndJoin()代码根本没机会执行,最终测试因超时失败
3. 替换为delay(1)后正常工作的原因
当使用delay(1)时:
backgroundScope.launch { while(isActive) { delay(1) // 触发调度器推进时间,切换任务 } }
- 协程执行到
delay(1)时,会触发TestDispatcher模拟时间流逝,此时调度器会暂停当前协程,转而执行主线程中等待的delay(1000) delay(1000)执行完成后,调用job.cancelAndJoin():此时协程的isActive状态变为false- 调度器回到之前的协程,下次执行
delay(1)时会检查取消状态,抛出CancellationException,循环终止,测试正常完成
验证与解决思路
如果要让yield()的测试正常工作,需要手动让调度器切换任务,比如在启动协程后调用advanceUntilIdle(),让初始的yield()循环先执行一次并让出线程:
@Test fun `should cancel with yield`() = runTest { val job = backgroundScope.launch { while(isActive) { yield() } } advanceUntilIdle() // 让协程执行一次yield并让出线程 delay(1000) job.cancelAndJoin() assertTrue { job.isCancelled } }
这样就能保证后续的delay(1000)和取消逻辑有机会执行。
内容的提问来源于stack exchange,提问作者TickL Me Tibbers
相关产品推荐
相关产品推荐

