问题背景
在多线程编程中,保证并发任务的顺序执行是一个常见且重要的需求。本文将探讨几种在Kotlin协程中实现多线程顺序执行的方案。
考虑这样一个场景:有5个线程同时提交任务,每个任务都需要进行耗时操作,要求这些任务的回调必须按照特定顺序执行。
private const val THREAD_NUM = 5
private var mCalculateNum = 0
几种解决方式
1、Dispatchers.IO.limitedParallelism(1) 受限并行度的协程调度器
@OptIn(ExperimentalCoroutinesApi::class)
private val mSequentScope = CoroutineScope(Dispatchers.IO.limitedParallelism(1))
private fun processData(callback: (Int) -> Unit) {
mSequentScope.launch {
delay((500..1500).random().toLong())
val result = ++mCalculateNum
withContext(Dispatchers.Main) {
callback.invoke(result)
if (result == THREAD_NUM) mSequentScope.cancel()
}
}
}
//调用它:
repeat(THREAD_NUM) { index ->
Thread {
//Dispatchers.IO.limitedParallelism(1)方式
processData { result -> log("线程$index, 回调结果: $result") }
//3、Mutex加锁方式
processWithLock { result -> log("线程$index, 回调结果: $result") }
}.start()
}
执行结果:
16:41:38.017 开始执行
16:41:38.582 线程4, 回调结果: 1
16:41:38.750 线程0, 回调结果: 2
16:41:38.779 线程2, 回调结果: 3
16:41:39.112 线程3, 回调结果: 4
16:41:39.231 线程1, 回调结果: 5
Dispatchers.IO.limitedParallelism(1) 创建了一个最大并行度为1的协程调度器,在主线程中多次调用processData()后,启动的多个线程会顺序执行。
2、Mutex互斥锁保护临界区
private val mSyncLock = Mutex()
private val mScope = CoroutineScope(Dispatchers.Main)
private fun processWithLock(callback: (Int) -> Unit) {
mScope.launch {
mSyncLock.withLock {
val result = withContext(Dispatchers.IO) {
delay((500..1500).random().toLong())
++mCalculateNum
}
callback.invoke(result)
}
}
}
//调用它:
repeat(THREAD_NUM) { index ->
Thread {
//Mutex加锁方式
processWithLock { result -> log("线程$index, 回调结果: $result") }
}.start()
}
执行结果:
16:47:40.126 开始执行
16:47:40.735 线程2, 回调结果: 1
16:47:42.034 线程1, 回调结果: 2
16:47:43.311 线程4, 回调结果: 3
16:47:44.452 线程0, 回调结果: 4
16:47:45.576 线程3, 回调结果: 5
使用Mutex互斥锁保护共享资源mCalculateNum的访问, withLock扩展函数确保同一时间只有一个协程能够执行临界区代码。耗时操作在IO线程执行,回调在主线程执行。
3、Channel队列处理器
/**
* 使用Channel来处理
*/
class EventHelper {
private val channel = Channel<Event>(Channel.UNLIMITED)
private val mChannelScope = CoroutineScope(Dispatchers.IO)
private val isActive = AtomicBoolean(true)
//接收事件
fun startProcess() {
mChannelScope.launch {
/**
* Channel实现了ReceiveChannel(提供了iterator方法)接口,而这个接口通过迭代器模式提供了协程化的遍历能力。
* 迭代器的hasNext()和next()都是挂起函数,能在没有元素时自动挂起协程,for循环会持续从Channel中接收元素,直到Channel被关闭。
*/
for (event in channel) {
if (!isActive.get()) break //停止读取
delay((500..1500).random().toLong()) //模拟在子线程的耗时处理
withContext(Dispatchers.Main) {
//在主线程展示数据
if (isActive.get()) {
log("接收event: $event")
}
}
}
}
}
/**
* 发送事件,注意这里不能使用trySend。
*
* 1、trySend():非阻塞尝试发送,立即返回结果,成功返回Success,失败返回Closed或队列已满,适用于非关键事件,允许丢失,避免协程挂起。
* 2、send():挂起发送,如果Channel已满,挂起协程直到有空间。适用于需要确保事件一定被发送,不丢失数据。
*/
suspend fun sendEvent(event: Event) {
if (isActive.get()) {
channel.send(event)
log("发送event:$event")
}
}
fun stop() {
isActive.set(false)
mChannelScope.cancel()
channel.close()
}
}
data class Event(val data: String, val index: Int)
调用处:
private val mScope = CoroutineScope(Dispatchers.Main)
mScope.launch {
val channelHelper = EventHelper()
//尝试接收数据,如果数据为空会挂起
channelHelper.startProcess()
//发送数据
repeat(THREAD_NUM) { index ->
channelHelper.sendEvent(Event("data$index", index))
}
//监听生命周期,页面关闭时停止发送&接收
activity.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
channelHelper.stop()
}
})
}
执行结果:
21:02:33.352 E 发送event:Event(data=data0, index=0)
21:02:33.352 E 发送event:Event(data=data1, index=1)
21:02:33.352 E 发送event:Event(data=data2, index=2)
21:02:33.352 E 发送event:Event(data=data3, index=3)
21:02:33.352 E 发送event:Event(data=data4, index=4)
21:02:33.865 E 接收event: Event(data=data0, index=0)
21:02:35.331 E 接收event: Event(data=data1, index=1)
21:02:36.426 E 接收event: Event(data=data2, index=2)
21:02:37.719 E 接收event: Event(data=data3, index=3)
21:02:38.771 E 接收event: Event(data=data4, index=4)
以上也能实现对应效果,关于Channel的用法扩展一下,其构造方法中:
public fun <E> Channel(
capacity: Int = RENDEZVOUS,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,
onUndeliveredElement: ((E) -> Unit)? = null
): Channel<E> {
//......
}
capacity表示容量策略,通常有以下几种选择:
| 容量类型 | 发送行为 | 接收行为 | 适用场景 |
|---|---|---|---|
| RENDEZVOUS,默认值0 | 无缓冲区时挂起 | 无数据时挂起 | 严格同步的生产者消费者 |
| CONFLATED ,值=-1 | 永不挂起,覆盖旧值 | 正常接收 | 状态更新,只关心最新值 |
| BUFFERED ,值=-2 | 缓冲区满时挂起 | 正常接收 | 一般事件处理,应对突发 |
| UNLIMITED,值=Int.MAX_VALUE | 永不挂起 | 正常接收 | 绝对不能丢失数据的场景 |
| 固定数值 | 缓冲区满时挂起 | 正常接收 | 需要精确控制内存的场景 |
示例代码中选择的是Channel.UNLIMITED,保证了发送的数据不会丢失。Channel通过单消费者队列模型实现多线程串行执行:多个生产者线程并发发送的事件会在Channel的内部队列中缓冲排队,由单个消费者协程通过for (event in channel)循环按FIFO顺序逐个处理。这种"多线程并发发送、单线程顺序消费"的机制,结合Channel内部的线程安全保证,使得所有事件最终都能被串行化处理,从而实现了多线程环境下的顺序执行。
总结
- Dispatchers.IO.limitedParallelism(1) 受限并行度适合任务量已知的场景
- Mutex 互斥锁适合需要精细控制的并发访问
- Channel队列 适合生产者消费者模式,异步事件流处理