Kotlin协程并发控制:多线程环境下的顺序执行

问题背景

在多线程编程中,保证并发任务的顺序执行是一个常见且重要的需求。本文将探讨几种在Kotlin协程中实现多线程顺序执行的方案。

考虑这样一个场景:有5个线程同时提交任务,每个任务都需要进行耗时操作,要求这些任务的回调必须按照特定顺序执行

kotlin 复制代码
private const val THREAD_NUM = 5
private var mCalculateNum = 0

几种解决方式

1、Dispatchers.IO.limitedParallelism(1) 受限并行度的协程调度器

kotlin 复制代码
@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()
}

执行结果:

makefile 复制代码
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互斥锁保护临界区

kotlin 复制代码
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()
}

执行结果:

makefile 复制代码
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队列处理器

kotlin 复制代码
/**
 * 使用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)

调用处:

kotlin 复制代码
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()
        }
    })
}

执行结果:

ini 复制代码
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的用法扩展一下,其构造方法中:

kotlin 复制代码
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队列 适合生产者消费者模式,异步事件流处理
相关推荐
xiangpanf10 小时前
Laravel 10.x重磅升级:五大核心特性解析
android
robotx13 小时前
安卓线程相关
android
消失的旧时光-194313 小时前
Android 面试高频:JSON 文件、大数据存储与断电安全(从原理到工程实践)
android·面试·json
dalancon14 小时前
VSYNC 信号流程分析 (Android 14)
android
dalancon14 小时前
VSYNC 信号完整流程2
android
dalancon14 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户693717500138415 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android16 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才17 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android
阿明的小蝴蝶17 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle