Kotlin协程无法在不同线程执行的问题排查
协程测试SimulationService时主线程阻塞问题解析
问题场景
我有个SimulationService,核心逻辑是持续循环直到调用stop()方法。在Spring Boot Web环境里跑没问题——因为Spring的Web请求是线程隔离的,能通过修改布尔标志中断循环。
但用协程写测试时踩了坑:
internal class SimulationServiceTest { private val simulateservice = SimulationService() @Test fun `should update the amount of ticks ran by the simulation`(): Unit = runBlocking { assertThat(SimulationContext.ticks).isEqualTo(0) launch(start = CoroutineStart.UNDISPATCHED, context = Dispatchers.IO) { simulateservice.start() } sleep(1000) launch(start = CoroutineStart.UNDISPATCHED, context = Dispatchers.Default) { simulateservice.stop() } assertThat(SimulationContext.ticks).isGreaterThan(0) } }
测试直接卡死,主线程被start()的循环占住,协程根本没机会执行stop()。我本来以为协程会切换到新线程,而且Dispatchers.IO和Default是不同线程池,结果完全不是这么回事。
换成Java的Executors就正常:
internal class SimulationServiceTest { private val simulateservice = SimulationService() @Test fun `should update the amount of ticks ran by the simulation`() { assertThat(SimulationContext.ticks).isEqualTo(0) val executor = Executors.newSingleThreadExecutor() executor.submit { simulateservice.start() } sleep(1000) simulateservice.stop() assertThat(SimulationContext.ticks).isGreaterThan(0) } }
问题根源
两个关键点导致了这个问题:
CoroutineStart.UNDISPATCHED启动模式:这个模式会让协程直接在当前线程(也就是runBlocking所在的测试主线程)立即执行,完全忽略你指定的Dispatchers.IO/Default。结果就是simulateservice.start()的无限循环直接霸占了主线程,后面的sleep和第二个launch根本没机会跑。- 无挂起点的阻塞代码:协程的非阻塞是建立在有挂起点的基础上的。如果
start()的循环里没有任何挂起函数调用,协程会一直占着当前线程不放,和普通的线程阻塞代码没区别。
修复后的测试代码
去掉UNDISPATCHED启动模式,让协程正常调度到指定线程池,同时用协程的delay替代Thread.sleep,最后等待协程结束:
internal class SimulationServiceTest { private val simulateservice = SimulationService() @Test fun `should update the amount of ticks ran by the simulation`(): Unit = runBlocking { assertThat(SimulationContext.ticks).isEqualTo(0) // 让协程调度到IO线程池执行,不占用测试主线程 val simulationJob = launch(Dispatchers.IO) { simulateservice.start() } delay(1000) // 用协程挂起函数替代Thread.sleep,不阻塞主线程 simulateservice.stop() simulationJob.join() // 等待模拟任务结束 assertThat(SimulationContext.ticks).isGreaterThan(0) } }
额外提示
- 尽量用
delay()代替Thread.sleep(),前者是挂起函数,不会阻塞线程,能让其他协程正常调度。 - 如果
SimulationService的循环逻辑没法加挂起点,那它本质就是阻塞线程的代码,这时用协程和线程池效果类似,但一定要避免UNDISPATCHED模式,确保协程跑在独立线程上。 runBlocking会阻塞当前线程直到所有子协程完成,很适合在测试里包裹协程代码。
内容的提问来源于stack exchange,提问作者xetra11
相关产品推荐
相关产品推荐

