Kotlin:使用协程实现Executors.newSingleThreadExecutor的执行效果
问题背景
使用Executors.newSingleThreadExecutor时,任务会按注册顺序排队执行,输出严格按0到10的顺序。但尝试用Dispatchers.Default.limitedParallelism(1)实现相同效果时,输出却是奇数先完成,顺序不符合预期。
原因分析
limitedParallelism(1)仅限制同时运行的协程数量为1,但协程的挂起(比如delay)是非阻塞操作:当第一个协程(i=0)执行delay(2000)时,会让出当前线程,调度器会立即启动下一个等待的协程(i=1)。由于i=1的delay(1000)耗时更短,会先完成并输出,最终导致奇数先被打印。
而newSingleThreadExecutor中的Thread.sleep是阻塞线程的操作,线程被占用时无法处理其他任务,必须等当前任务完全执行完才会取下一个任务,因此能保证顺序。
解决方法
要实现协程任务的严格顺序执行,需确保前一个任务完全完成后,再启动下一个任务,有三种常用方案:
方案1:使用Mutex串行化任务
通过互斥锁(Mutex)将每个任务的执行逻辑包裹,确保同一时间只有一个协程执行任务块:
val mutex = Mutex() val dispatcher = Dispatchers.Default.limitedParallelism(1) (0..10).forEach { i -> lifecycleScope.launch(dispatcher) { mutex.withLock { if (i % 2 == 0) { delay(2000) } else { delay(1000) } println(i) } } }
withLock会自动处理锁的获取与释放,即使协程挂起(比如delay),锁也会保持,直到当前任务块执行完毕,下一个协程才能获取锁执行。
方案2:顺序启动并等待协程完成
在同一个协程中,逐个启动任务协程并调用join()等待其完成,再启动下一个:
lifecycleScope.launch { (0..10).forEach { i -> launch { if (i % 2 == 0) { delay(2000) } else { delay(1000) } println(i) }.join() // 等待当前协程执行完毕,再处理下一个任务 } }
这种方式更直观,完全按任务注册顺序依次执行,无需额外调度器配置。
方案3:使用Channel实现任务队列
通过Channel作为任务队列,单个消费者协程按顺序处理任务:
val taskChannel = Channel<Int>(Channel.UNLIMITED) // 启动单个消费者协程处理任务 lifecycleScope.launch { for (i in taskChannel) { if (i % 2 == 0) { delay(2000) } else { delay(1000) } println(i) } } // 发送任务到队列 (0..10).forEach { i -> lifecycleScope.launch { taskChannel.send(i) } }
这种方式适合任务动态添加的场景,消费者协程会严格按接收顺序处理每个任务。
总结
limitedParallelism(1)无法保证任务执行顺序的核心是协程挂起会让出线程,要实现严格串行执行,需通过互斥锁、顺序等待或任务队列的方式,确保任务按注册顺序依次完成。
内容的提问来源于stack exchange,提问作者Bugs Happen

