【Kotlin 协程修仙录 · 大乘境 · 初阶】 | 跳出三界:协程在 KMP 与后端开发中的跨平台之道

107 阅读10分钟

image_11.png

前言

合体大圆满,你已将协程与 Android 四大组件完美融合。CoroutineWorkerRoomRetrofitCompose 皆与协程浑然一体,你的 Android 协程修为已臻化境。

然而,一个全新的世界正在向你招手:

公司决定采用 Kotlin Multiplatform(KMM)统一 iOS 和 Android 的业务逻辑。你需要在 iOS 上也能运行协程?Swift 的异步怎么办? 后端团队开始用 Ktor 搭建服务,他们也在用协程处理高并发。后端协程和 Android 协程是一回事吗? 你听说协程可以编译成 WebAssembly,甚至能跑在 JavaScript 引擎上。协程的“挂起”魔法在单线程的 JS 里如何实现?

这些问题指向协程的更高境界——跨平台与后端开发。协程从来不只是 Android 的专属武器。它是 Kotlin 语言级的并发框架,理论上可以运行在任何支持 Kotlin 的平台上。理解协程的跨平台本质,你才能真正跳出 Android 的桎梏,看到协程设计的大道真意。

本讲是大乘境的初阶修炼。你将跳出 Android 三界,俯瞰协程的跨平台天地:

  • 理解 Kotlin 协程的多平台架构:commonjvmjsnative 模块的分工。
  • 掌握 KMM 中如何共享协程代码,以及在 iOS 上的实际运行机制。
  • 对比 Android 协程与后端 Ktor 协程的异同——线程池、调度器、并发模型。
  • 了解协程在 JavaScript 单线程环境中的实现原理(Promise 转换)。
  • 展望协程在未来的演进方向(虚拟线程、结构化并发推广)。

准备好跳出三界,以更高维度审视协程了吗?我们开始。

千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意


协程的多平台架构:一套代码,四处运行

KMP 的协程模块

Kotlin 协程库从设计之初就考虑了多平台支持。打开协程的源码仓库,你会看到这样的模块结构:

kotlinx.coroutines/
├── common/           # 跨平台公共代码
├── jvm/              # JVM 平台特定实现
├── js/               # JavaScript 平台特定实现
├── native/           # Native (iOS/macOS/Linux) 平台特定实现
└── android/          # Android 平台扩展

common 模块定义了协程的核心抽象:suspend 关键字、CoroutineScopeJobChannelFlow 等 API 的接口和部分实现。各平台模块则负责实现与底层线程/调度系统的对接。

flowchart LR
    subgraph Common[common 公共模块]
        C1[suspend 抽象]
        C2[CoroutineScope 接口]
        C3[Flow / Channel API]
    end
    
    subgraph JVM[JVM 平台]
        J1[Thread 线程池]
        J2[CoroutineScheduler]
    end
    
    subgraph Native[Native 平台 iOS]
        N1[Grand Central Dispatch]
        N2[Worker 线程]
    end
    
    subgraph JS[JavaScript 平台]
        S1[Promise / Event Loop]
        S2[单线程调度]
    end
    
    Common --> JVM
    Common --> Native
    Common --> JS
    
    style Common fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
    style JVM fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
    style Native fill:#fff3e0,stroke:#f57c00,stroke-width:2px
    style JS fill:#ffcdd2,stroke:#b71c1c,stroke-width:2px

核心设计哲学:协程的“挂起与恢复”是平台无关的(通过状态机和 Continuation),但“挂起后线程的调度”是平台相关的。这种分离使得协程能横跨 JVM、iOS、JS 三大生态。

KMM 中共享协程代码

KMM 项目的典型结构:

shared/
├── commonMain/       # 公共业务逻辑,包含 suspend 函数、Flow 等
├── androidMain/      # Android 特定实现(如 Dispatchers.Main)
└── iosMain/          # iOS 特定实现

commonMain 中,你可以像在 Android 中一样写协程代码:

// shared/commonMain/kotlin/UserRepository.kt
expect class PlatformDispatcher {
    val io: CoroutineDispatcher
    val main: CoroutineDispatcher
}

class UserRepository(private val api: UserApi) {
    suspend fun fetchUser(id: String): User {
        return withContext(PlatformDispatcher.io) {
            api.getUser(id)
        }
    }
}

androidMainiosMain 中分别提供 actual 实现:

// androidMain
actual class PlatformDispatcher {
    actual val io = Dispatchers.IO
    actual val main = Dispatchers.Main
}

// iosMain
actual class PlatformDispatcher {
    actual val io = Dispatchers.Default // iOS 上通常用 Default
    actual val main = Dispatchers.Main
}

这样,核心业务逻辑的协程代码就可以一次编写,双端共享


iOS 上的协程:GCDWorker 的幕后功臣

协程如何在 iOS 上运行?

Kotlin/Native 将 Kotlin 代码编译为 LLVM 字节码,最终生成 iOS 可执行的二进制。协程在 iOS 上的调度器,底层绑定的是 Apple 的 Grand Central Dispatch(GCD)Worker 线程机制。

  • Dispatchers.Main:绑定到主队列 dispatch_get_main_queue()
  • Dispatchers.Default:使用 CPU 核心数个 Worker 线程的池。
  • Dispatchers.IO:在 Kotlin/Native 中目前与 Default 相同(未来可能优化)。
flowchart LR
    subgraph Kotlin[Native 协程]
        S[suspend 函数]
        D[Dispatchers.Main]
    end
    
    subgraph iOS[iOS 底层]
        GCD[GCD 主队列]
        Worker[Worker 线程池]
    end
    
    S --> D --> GCD
    S --> IO[Dispatchers.IO] --> Worker
    
    style Kotlin fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
    style iOS fill:#e3f2fd,stroke:#1976d2,stroke-width:2px

Swift 异步的互操作

Swift 5.5 引入了 async/await,其底层同样是基于 GCD 的协程(Swift 称为“任务”)。Kotlin 协程与 Swift 异步可以通过回调或 suspend 函数的导出进行互操作。例如,你可以将一个 Kotlin 的 suspend 函数导出为 Swift 的 async 函数,让 iOS 开发者像调用原生异步函数一样使用。

// Kotlin 侧
@Throws(IOException::class)
suspend fun fetchData(): String = ...

// 导出后,Swift 侧可以这样调用:
// Task {
//     let data = try await FetchDataKt.fetchData()
// }

这种互操作能力,使得协程成为 KMM 中统一异步逻辑的最佳选择。


后端协程:Ktor 与高并发场景的协程实践

后端协程与 Android 协程的异同

对比维度Android 协程后端协程(Ktor/Spring)
主线程调度器Dispatchers.Main(UI 线程)通常无主线程概念
IO 调度器弹性线程池(最大 64)同样弹性,但可配置更大
典型并发模型少量协程处理 UI 事件海量协程处理网络请求(C10K)
生命周期管理依赖 viewModelScope 等 Android 组件依赖请求上下文(如 Ktor 的 call
结构化并发同样遵循同样遵循,但更强调请求边界
flowchart LR
    subgraph Android[Android 协程]
        A1[Dispatchers.Main UI]
        A2[viewModelScope]
        A3[lifecycleScope]
    end
    
    subgraph Backend[后端协程]
        B1[Dispatchers.IO / Default]
        B2[请求级 CoroutineScope]
        B3[应用级 SupervisorJob]
    end
    
    style Android fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
    style Backend fill:#ffcdd2,stroke:#b71c1c,stroke-width:2px

Ktor 中的协程:请求即协程

Ktor 是 Kotlin 官方推出的异步 Web 框架,它从设计之初就完全基于协程。每一个 HTTP 请求都在一个协程中处理,框架自动管理协程的生命周期。

// Ktor Application.kt
fun Application.module() {
    routing {
        get("/user/{id}") {
            val id = call.parameters["id"] ?: throw BadRequestException("Missing id")
            val user = userRepository.findById(id) // suspend 函数
            call.respond(user)
        }
    }
}

在 Ktor 中,你可以直接调用 suspend 函数进行数据库查询、网络调用,而无需关心线程池的细节。框架使用 Dispatchers.IO 或自定义调度器处理阻塞操作,并通过结构化并发确保请求结束时所有子协程被自动取消。

高并发下的协程优势

传统 Servlet 容器(如 Tomcat)为每个请求分配一个线程。当并发请求达到数千时,线程上下文切换的开销会急剧上升,内存也会被线程栈耗尽。

协程后端框架(Ktor、Spring WebFlux with coroutines)则可以为每个请求分配一个协程。协程的内存占用极小(几百字节),且能在 I/O 等待时让出线程,使得少量线程就能支撑海量并发。

flowchart LR
    subgraph ThreadModel[线程模型 每请求一线程]
        R1[请求1] --> T1[线程1]
        R2[请求2] --> T2[线程2]
        R3[请求3] --> T3[线程3]
        R1000[请求1000] --> T1000[线程1000 内存爆]
    end
    
    subgraph CoroutineModel[协程模型 少量线程]
        C1[协程1] --> W1[Worker线程1]
        C2[协程2] --> W1
        C3[协程3] --> W2[Worker线程2]
        C1000[协程1000] --> W2
    end
    
    style ThreadModel fill:#ffcdd2,stroke:#b71c1c,stroke-width:2px
    style CoroutineModel fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px

JavaScript 单线程上的协程:Promise 的华丽转身

JS 平台的协程实现

Kotlin/JS 将 Kotlin 代码编译为 JavaScript,运行在浏览器或 Node.js 的单线程事件循环上。协程的“挂起”在 JS 中如何实现不阻塞事件循环?

答案在于 Promise。Kotlin/JS 的协程编译器会将 suspend 函数转换为返回 Promise 的 JavaScript 函数。状态机的挂起与恢复,最终映射为 Promise.then() 的链式调用。

// Kotlin suspend fun fetchData(): String
// 编译后大致等价于:
function fetchData(continuation) {
    return new Promise((resolve, reject) => {
        // 状态机逻辑...
    });
}
flowchart LR
    subgraph Kotlin[Kotlin 源码]
        S[suspend fun]
    end
    
    subgraph Compiler[编译器]
        C[转换为状态机 + Promise]
    end
    
    subgraph JS[JavaScript 运行时]
        P[Promise]
        EL[Event Loop]
    end
    
    S --> C --> P --> EL
    
    style Kotlin fill:#c8e6c9,stroke:#2e7d32
    style Compiler fill:#fff3e0,stroke:#f57c00
    style JS fill:#ffcdd2,stroke:#b71c1c

JavaScript 异步互操作

你可以直接在 Kotlin/JS 中调用返回 Promise 的 JavaScript 函数,Kotlin 提供了 Promise<T>.await() 扩展函数,将其转换为 suspend 函数:

suspend fun fetchFromJsApi(): String {
    val promise = js("fetch('/api/data').then(res => res.text())")
    return promise.await()
}

同样,Kotlin 的 suspend 函数也可以导出为返回 Promise 的函数,供 JavaScript 代码调用。


协程的未来:虚拟线程与结构化并发的推广

Project Loom 与协程的殊途同归

Java 19 引入了虚拟线程,其设计目标与 Kotlin 协程高度相似:让开发者以同步代码风格编写高并发程序,且不阻塞平台线程。

对比项Kotlin 协程Java 虚拟线程
挂起机制编译器生成状态机JVM 底层支持,可挂起任意栈帧
语言关键字suspend无需关键字,普通方法即可
调度器用户态调度器JVM 内置调度器
成熟度生产可用多年较新,生态仍在建设

虚拟线程的普及可能会让协程在后端领域面临竞争,但在 Android 和 KMM 领域,协程的地位仍然不可撼动。

结构化并发的跨语言影响

Kotlin 协程提出的结构化并发理念,正在影响其他语言的异步设计。Swift 的 TaskTaskGroup、Rust 的 async 作用域、甚至 Java 的 StructuredTaskScope,都在不同程度上借鉴了“父子协程生命周期约束”的思想。

协程的修仙之路,不仅在 Kotlin 内部延续,更在塑造整个异步编程的未来。

flowchart LR
    subgraph Kotlin[Kotlin 结构化并发]
        K1[coroutineScope]
        K2[supervisorScope]
    end
    
    subgraph Influence[跨语言影响]
        I1[Swift TaskGroup]
        I2[Java StructuredTaskScope]
        I3[Rust async scopes]
    end
    
    Kotlin --> Influence
    
    style Kotlin fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
    style Influence fill:#e3f2fd,stroke:#1976d2,stroke-width:2px

实战:KMM 共享 ViewModel 中的协程

让我们构建一个 KMM 项目中的共享 ViewModel,使用协程加载数据。

// commonMain/kotlin/SharedViewModel.kt
expect class SharedViewModel() {
    val uiState: StateFlow<UiState>
    fun loadData()
}

// androidMain/kotlin/SharedViewModel.kt
actual class SharedViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    actual val uiState: StateFlow<UiState> = _uiState.asStateFlow()
    
    actual fun loadData() {
        viewModelScope.launch {
            _uiState.value = try {
                val data = repository.fetchData()
                UiState.Success(data)
            } catch (e: Exception) {
                UiState.Error(e.message)
            }
        }
    }
}

// iosMain/kotlin/SharedViewModel.kt
actual class SharedViewModel {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    actual val uiState: StateFlow<UiState> = _uiState.asStateFlow()
    
    actual fun loadData() {
        scope.launch {
            _uiState.value = try {
                val data = repository.fetchData()
                UiState.Success(data)
            } catch (e: Exception) {
                UiState.Error(e.message)
            }
        }
    }
    
    fun clear() {
        scope.cancel()
    }
}

在 iOS 侧,你需要在合适的时机调用 clear() 来取消协程(例如在 deinit 中)。

flowchart LR
    subgraph Common[commonMain 共享]
        API[expect SharedViewModel]
        State[UiState StateFlow]
    end
    
    subgraph Android[androidMain]
        VM1[actual ViewModel]
        Scope1[viewModelScope]
    end
    
    subgraph iOS[iosMain]
        VM2[actual 普通类]
        Scope2[手动管理 CoroutineScope]
    end
    
    API --> VM1
    API --> VM2
    VM1 --> Scope1
    VM2 --> Scope2
    
    style Common fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
    style Android fill:#c8e6c9,stroke:#388e3c
    style iOS fill:#e3f2fd,stroke:#1976d2

常见误区与避坑指南

误区 1:认为 KMMDispatchers.MainiOS 上不可用

Dispatchers.Main 在 Kotlin/Native 的 iOS 目标上是完全可用的,它绑定到主线程的 GCD 队列。你可以在 iosMain 中安全地使用它来更新 UI(通过 Compose Multiplatform 或其他 UI 框架)。

误区 2:在后端中滥用 GlobalScope

// 后端错误示例
fun handleRequest() {
    GlobalScope.launch {
        // 请求结束时协程不会自动取消
        process()
    }
}

正确做法:使用请求级别的 CoroutineScope,或框架提供的 Scope。

误区 3:在 JS 平台使用 runBlocking

runBlocking 在 JS 平台上不可用,因为 JS 是单线程、无法阻塞事件循环。所有协程启动必须使用 launchasync 配合回调/Promise。


最佳实践

  1. 在 KMM 中,将 suspend 函数和 Flow 定义在 commonMain:最大化代码复用。
  2. 使用 expect/actual 为不同平台提供调度器配置:而不是在公共代码中硬编码 Dispatchers.IO
  3. 后端开发中,始终使用请求级或应用级的 CoroutineScope:避免协程泄漏。
  4. 在 JS 平台,优先使用 Promise.await() 与 JavaScript 异步互操作
  5. 关注 Project Loom 与协程的演进,适时调整技术选型

九、总结与下回预告

恭喜,你已跳出 Android 三界,看到了协程在跨平台与后端开发的广袤天地!大乘境初阶修炼完成!

本讲核心收获

  • Kotlin 协程通过 common 模块定义抽象,各平台负责底层实现。
  • iOS 上的协程基于 GCD 和 Worker 线程,可与 Swift 异步互操作。
  • 后端协程(Ktor)以请求为 Scope,少量线程支撑海量并发。
  • JS 平台将协程编译为 Promise,运行于单线程事件循环。
  • 结构化并发的理念正在影响 Swift、Java、Rust 等语言。

在下一讲 【大乘境·中阶】 中,我们将深入协程的源码核心:CoroutineScheduler 的任务分发算法、DispatchedContinuation 的拦截机制、以及协程的启动流程全解析。届时你将真正看透协程的每一行源码。


【当前境界修为面板】

当前境界修炼技能修炼进度修炼心得
大乘境 · 初阶1、KMM 跨平台协程诀
2、Ktor 后端协程术
3、JS Promise 转换法
当前进度35%
修为350/1000
下一突破[大乘境 · 中阶] (需领悟:CoroutineScheduler 源码、DispatchedContinuation 拦截机制、协程启动流程)
协程不限于Android。一套suspend代码,横跨JVMiOSJS三界。

【本讲思考题】

  1. 表象题:Kotlin 协程在 iOS 上的 Dispatchers.Main 底层绑定的是什么?

  2. 场景题:你需要设计一个 KMM 共享的 ImageLoader,它使用协程加载网络图片,并需要在 Android 和 iOS 上都能限制最大并发数为 5。如何设计这个类的 expect/actual 结构?

  3. 原理题:为什么 runBlocking 在 Kotlin/JS 平台上不可用?请从 JavaScript 事件循环的特性解释。


道友,大乘境的大门已敞开。协程的跨平台之道,让你不再局限于 Android 一隅。下一讲,我们将潜入协程源码的最深处,与 CoroutineSchedulerDispatchedContinuation 正面交锋。大乘境·中阶见。

欢迎一键四连关注 + 点赞 + 收藏 + 评论