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队列 适合生产者消费者模式,异步事件流处理
相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui