协程和多线程编程指引

大约 7 分钟

协程和多线程编程指引

在本章节,我们将学习如何在Kuikly中进行异步编程,讨论如何选择合适的异步编程方案。

概念和术语

异步编程涉及的概念和术语比较广泛,本小节将介绍一些核心概念和术语,如果你已经熟悉这些概念,可以跳过本小节。

Kuikly线程

Kuikly UI执行的线程,所有Kuikly的UI操作(View、Attr、Event、observable、setTimeout等)都只能在Kuikly线程调用

Kuikly Module机制

将Native的API暴露给kuikly使用的方案,可实现在Native的API中使用不同平台的多线程能力。

提示

具体可以参考Kuikly Module机制

KMP的expect和actual机制

expectactual是KMP的核心机制,用于定义和实现跨平台的接口,从而使用不同平台的多线程能力(以下简称KMP多线程)。

协程

协程是轻量级的并发单元,由程序逻辑控制调度而非操作系统,适用于异步任务和非阻塞操作。 协程的两个主要功能:1. 替代回调地狱,简化异步代码;2. 执行并发任务。

挂起函数(Suspend Function)

用 suspend 关键字标记的函数,可在不阻塞线程的情况下暂停和恢复执行。 挂起函数是 Kotlin 语言的核心特性,是协程语法的基础,在所有目标平台均可使用。 挂起函数只能在协程或其他挂起函数中调用。示例:

suspend fun fetchData(): String {
    delay(1000)
    return "data"
}

Kuikly协程API和依赖库

要在Kuikly中使用协程,首先需要了解以下三种API和依赖库:

Kuikly CoroutineScope API

CoroutineScope 是协程的上下文,用于执行挂起函数。Kuikly框架提供了自己的 CoroutineScope API(以下简称Kuikly内建协程),包括GlobalScopePager.lifecycleScope

Kuikly内建协程是在Kuikly线程中执行的,不会有线程切换的开销,也不会有线程安全问题。示例:

GlobalScope.launch {
    val data = fetchData()
    ...
}

备注

「不会有线程安全问题」是指Kuikly内建协程始终在Kuikly线程中执行,不会遇到线程安全问题,需要注意的是,Kuikly内建协程API本身非线程安全,因此不能在Kuikly线程外调用。

kotlinx.coroutines库

kotlinx.coroutines库是 Kotlin 官方提供的协程库(以下简称kotlinx协程),定义了协程的上下文(CoroutineScope)和调度器(Dispatchers)API,并在不同目标平台提供了不同的实现。应用也可以自定义自己的协程作用域和调度器。

通过调度器指定协程运行的线程或线程池。示例:

GlobalScope.launch {
    println("running in default dispatcher")
    withContext(Dispatchers.Main) {
        println("running in Main dispatcher")
    }
}

提示

不同平台支持的调度器有所不同,例如,除了各平台共有的Dispatchers.DefaultDispatchers.Unconfined,Android平台还提供了Dispatchers.MainDispatchers.IO等。 具体可以参考kotlinx.coroutines的API文档open in new window

kuiklyx.coroutines库

前面提到,Kuikly UI操作都只能在Kuikly线程调用。kuiklyx.coroutines库(以下简称kuiklyx协程)提供了切换到kuikly线程的能力,当我们在非Kuikly线程执行异步任务后,就可以通过kuiklyx协程切换到Kuikly线程进行UI操作。

private suspend fun fetchData() = withContext(Dispatchers.Main) {
    // 在Main线程执行异步任务
    return@withContext "mock"
}

override fun created() {
    super.created()
    val ctx = this
    GlobalScope.launch(Dispatchers.Kuikly[ctx]) {
        // 调用挂起函数fetchData
        val data = ctx.fetchData()
        // 切换到Kuikly线程更新响应式字段
        ctx.dataObservable = data
    }
}

kuiklyx协程使用方法

dependencies {
    implementation("com.tencent.kuiklyx-open:coroutines:$KUIKLYX_COROUTINES_VERSION")
}
  • 协程方式API
// case 0: js模式使用前需要触发KuiklyContextScheduler初始化,其它模式可忽略
KuiklyContextScheduler
// case 1: 启动协程
GlobalScope.launch(Dispatchers.Kuikly[ctx]) { ... }
// case 2: 在协程中切换上下文
withContext(Dispatchers.Kuikly[ctx]) { ... }
  • 回调方式API
KuiklyContextScheduler.runOnKuiklyThread(pagerId) { cancel ->
    if (cancel) {
        // pager is destroyed
        return
    }
    // do something
}

Kuikly异步编程介绍

协程多线程是Kuikly异步编程的两个维度,它们可以组合出不同的异步编程方式,每种方式都有其适用的场景和优缺点。

多线程实现方式:

Module机制KMP多线程
动态化支持不支持
依赖库包增量kuiklyx协程库
切换到Kuikly线程框架自动完成需要主动切换
通信开销

协程实现方式:

回调(无协程)Kuikly内建协程kotlinx协程
动态化支持支持不支持
依赖库包增量kotlinx协程库
线程安全不涉及自动保障需要考虑

选择合适的异步编程方式

方式1:Module机制和(或)Kuikly内建协程

场景一

需要使用协程语法代替回调,提升代码可读性,没有多线程诉求:建议使用Kuikly内建协程。

示例:

private suspend fun fetchLocal(): Int {
    return suspendCoroutine { ... } // 把回调式API转换为挂起函数
}

private suspend fun fetchRemote(type: Int): String {
    return suspendCoroutine { ... } // 把回调式API转换为挂起函数
}

override fun created() {
    super.created()
    val ctx = this
    lifecycleScope.launch { // 通过Pager.lifecycleScope启动协程
        val type = fetchLocal() // 调用挂起函数fetchLocal
        val data = fetchRemote(type) // 调用挂起函数fetchRemote
        ctx.dataObservable = data // 更新响应式字段
    }
}

场景二

需要执行耗时任务,同时需要支持动态化:建议使用Module机制,将耗时任务放到平台侧通过原生能力实现。

示例:

override fun created() {
    super.created()
    val ctx = this
    ctx.loadingObservable = true
    val module = acquireModule<FetchDataModule>(FetchDataModule.MODULE_NAME)
    module.fetchData {
        ctx.dataObservable = it
        ctx.loadingObservable = false
    }
}

提示

  • FetchDataModule是通过Module机制预先实现的原生扩展API
  • Module机制可以配合场景一的Kuikly内建协程使用

方式2: KMP多线程和Kuiklyx协程库(回调方式)

场景三

需要执行耗时任务,对通信开销要求较高,不考虑动态化,跨端逻辑相对简单不需要协程能力:建议使用KMP多线程自行切换线程,再通过kuiklyx协程库(回调方式)回到Kuikly线程更新UI。

示例:

先在build.gradle.kts添加依赖库:

val commonMain by getting {
    dependencies {
        // kuiklyx协程库
        implementation("com.tencent.kuiklyx-open:coroutines:$KUIKLYX_COROUTINES_VERSION")
    }
}

提示

KUIKLYX_COROUTINES_VERSION需要根据项目使用的kotlin版本,替换为对应的版本号

然后在Kuikly页面中使用:

override fun created() {
    super.created()
    val ctx = this
    ctx.loadingObservable = true // 更新loading状态
    // 调用KMP方法
    asyncKmpFetchData { data ->
        KuiklyContextScheduler.runOnKuiklyThread(ctx.pagerId) { cancel ->
            if (cancel) {
                return
            }
            // 回到Kuikly线程,更新响应式字段
            ctx.dataObservable = data
            ctx.loadingObservable = false
        }
    }
}

方式3:Kotlinx协程库和Kuiklyx协程库(协程方式)

场景四

需要使用多线程能力,对通信开销要求较高,不考虑动态化,同时需要协程语法提升代码可读性:建议使用kotlinx协程,再通过kuiklyx协程库(协程方式)回到Kuikly线程更新UI。

示例:

先在build.gradle.kts添加依赖库:

val commonMain by getting {
    dependencies {
        // kotlinx协程库
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$KOTLINX_COROUTINES_VERSION")
        // kuiklyx协程库
        implementation("com.tencent.kuiklyx-open:coroutines:$KUIKLYX_COROUTINES_VERSION")
    }
}

提示

KOTLINX_COROUTINES_VERSIONKUIKLYX_COROUTINES_VERSION需要根据项目使用的kotlin版本,替换为对应的版本号

然后在Kuikly页面中使用:

override fun created() {
    super.created()
    val ctx = this
    // 使用kotlinx协程库启动协程,通过kuiklyx协程库切换到Kuikly线程
    GlobalScope.launch(Dispatchers.Kuikly[ctx]) {
        ctx.loadingObservable = true // 更新loading状态
        val data = withContext(Dispatchers.IO) { // 在IO线程调用KMP的方法
            kmpFetchData()
        }
        // 回到Kuikly线程,更新响应式字段
        ctx.dataObservable = data
        ctx.loadingObservable = false
    }
}

提示

  • Dispatchers.KuiklyPager相关,需要通过Pager上下文获取
  • Dispatchers.IO是通过KMP方式实现的自定义调度器
  • KMP多线程不支持动态化,是因为动态化需要的js目标平台不支持多线程

更多方式

上述列举了3种常用方式和对应的场景示例,实际开发中,还可以组合出更多的异步编程方式,此处不再赘述。

关于线程安全

  • KMP多线程需要开发者自行考虑线程安全问题,可以借助kotlinx:atomicfu库提供的原子操作和同步锁能力;
  • Kuikly UI的相关类(View、Attr、Event、ObservableProperties、GlobalFunctions等)非线程安全,且只能在Kuikly线程访问。
上次编辑于: