Kotlin 协程基础

作为 Java 开发者,你习惯用 ThreadExecutorServiceCompletableFuture 处理并发。Kotlin 协程是更轻量级的方案——它不是线程的替代品,而是一种可挂起计算的抽象,可以在不阻塞线程的前提下暂停和恢复。

NOTE

关键认知 协程运行在线程之上。一个线程可以运行成千上万个协程。协程的挂起是零成本的(不像阻塞线程那样耗资源)。

Java 的并发是”重资源、显式管理”;Kotlin 协程是”轻资源、结构化管理”。

1. 协程 vs 线程:宏观对比

概念JavaKotlin
启动异步任务new Thread(runnable).start()scope.launch { ... }
获取异步结果Future.get() → 阻塞async { ... }.await() → 挂起
线程池ExecutorServiceDispatchers.IO / Default / Main
任务取消future.cancel()scope.cancel() / 协程自动传播取消
异常处理try-catch 围绕 get()try-catch 在协程内部,或 CoroutineExceptionHandler
组合异步操作CompletableFuture.thenCombine()async + await()coroutineScope { }

TIP

理解关键 协程的”挂起” != 线程的”阻塞”。挂起时协程释放线程去做其他工作,等条件满足后再恢复——整个过程不需要额外线程。

2. launch:启动并忘记

launch 是最常用的协程构建器,它启动一个不返回结果的协程(类似于 new Thread(runnable),但轻量得多)。

// ☕ Java 线程
new Thread(() -> {
    doWork();
    doMoreWork();
}).start();
// 无法结构化取消
// 🚀 Kotlin launch
scope.launch {
    doWork()
    doMoreWork()
}
// scope.cancel() 可取消所有协程

TIP

🔑 PICO Demo 实战 几乎所有项目都会在 mainApp() 中使用 MainScope().launch { assetBundle.await() } 来启动异步资源加载。

ViewModel 中更是随处可见 viewModelScope.launch { ... }——这是 Android 为 ViewModel 提供的生命周期感知协程作用域。

3. async/await:获取异步结果

当需要异步结果时使用 async,它返回一个 Deferred<T>(类似于 Java 的 Future<T>)。

// ☕ CompletableFuture
CompletableFuture<Data> future =
    CompletableFuture.supplyAsync(() -> {
        return loadData();
    });
Data data = future.get(); // 阻塞调用线程
// 🚀 Kotlin async
val deferred: Deferred<Data> = scope.async {
    loadData()
}
val data = deferred.await() // 挂起,不阻塞

核心差异future.get() 阻塞当前线程;deferred.await() 挂起当前协程,不阻塞线程。

TIP

🔑 PICO Demo 实战 多个项目使用 async(start = CoroutineStart.LAZY) 创建延迟加载的资源:

// 定义时不会立即执行——只有 .await() 时才触发
val assetBundle = CoroutineScope(Dispatchers.IO).async(
    start = CoroutineStart.LAZY
) {
    AssetBundle.load(BUNDLE_URI)
}
 
// 在其他地方消费时自动触发加载
MainScope().launch {
    val bundle = assetBundle.await()
}

4. 挂起函数:suspend 关键字

suspend 是 Kotlin 协程的核心。挂起函数可以挂起协程的执行,而不会阻塞其所在线程。

// 挂起函数只能在协程中或其他挂起函数中调用
suspend fun loadData(): Data {
    // 这个调用可能挂起当前协程
    return withContext(Dispatchers.IO) {
        // 在 IO 线程池中执行耗时操作
        database.query()
    }
    // 协程恢复后,回到原来的调度器(通常是 Main)
}
 
// 调用
scope.launch {
    val data = loadData()  // 可能挂起,但不阻塞 UI
    updateUI(data)         // 恢复后继续执行
}

TIP

🔑 PICO Demo 实战 所有 SDK 的异步操作都是挂起函数。看 VideoViewModel.kt

// 挂起函数初始化视频
suspend fun initialize(converter: PhysicalLengthConverter) {
    val bundle = videoAssetBundle.await()
    // IO 密集型操作
    val mediaPlayer = withContext(Dispatchers.IO) {
        bundle.loadMediaPlayer(mediaPath)
    }
    // 回到主线程更新 UI 状态
    _uiState.value = _uiState.value.copy(isInitialized = true)
}

5. 调度器:Dispatchers

Kotlin 用 Dispatchers 控制协程在哪个线程池执行,相当于 Java 的 ExecutorService

调度器用途类比 Java
Dispatchers.MainUI 操作(Compose 更新)Platform.runLater() (JavaFX)
Dispatchers.IO网络请求、文件读写、数据库Executors.newCachedThreadPool()
Dispatchers.DefaultCPU 密集型计算Executors.newFixedThreadPool(n)
// PICO Demo 中的标准模式:用 withContext 切换调度器
scope.launch {                          // 默认 Main 调度器
    val model = withContext(Dispatchers.IO) {
        Entity.load(STATIC_ROBOT)       // IO 操作
    }
    // 回到 Main,更新 UI
    updateUI(model)
}

6. 协程作用域:结构化并发

Java 的线程缺乏结构化管理——线程一旦启动就无法自动取消。Kotlin 通过协程作用域实现结构化并发。

作用域生命周期在 PICO Demo 中的用途
viewModelScopeViewModel 销毁时自动取消ViewModel 中的异步操作(5 个 Demo 使用)
MainScope()手动管理(一般在 mainApp 中使用)4 个 Demo 在 Main.kt 中用于资源加载
rememberCoroutineScope()随 Compose 组件的生命周期Composable 中的按钮点击、生命周期事件
自定义 CoroutineScope(SupervisorJob() + Dispatchers.Main)类内部管理Manager 类的异步操作(PhysicsManager 等)

TIP

🔑 为什么用 SupervisorJob? 在 PICO Demo 中多处看到 CoroutineScope(SupervisorJob() + Dispatchers.Main)——SupervisorJob 确保一个子协程失败不会取消其他兄弟协程。这对 Manager 类很重要,一个操作失败不应该影响其他并行任务。

7. PICO Demo 协程模式全景

综合 8 个 Demo 项目,协程的使用可以归纳为 5 种模式:

模式 1:延迟异步资源加载

// 在文件顶层定义
val assetBundle = CoroutineScope(Dispatchers.IO).async(
    start = CoroutineStart.LAZY
) { AssetBundle.load(BUNDLE_URI) }
 
// 使用时 .await() 触发加载
scope.launch { val bundle = assetBundle.await() }

模式 2:IO 密集型操作调度

// 几乎所有涉及 3D 模型、纹理、网格的操作
val result = withContext(Dispatchers.IO) {
    Entity.load(modelPath)
}

模式 3:ViewModel 中的生命周期感知协程

class MyViewModel : ViewModel() {
    init {
        viewModelScope.launch {
            // ViewModel 销毁时自动取消
            someFlow.collect { value ->
                _state.value = value
            }
        }
    }
}

模式 4:并行并发

suspend fun preloadAll() = coroutineScope {
    listOf(audio1, audio2, audio3).map { name ->
        async(Dispatchers.IO) {
            name to AudioResource.load(name = name, path = path)
        }
    }.awaitAll().forEach { (name, res) ->
        cache[name] = res
    }
}

模式 5:Composable 中的协程

@Composable
fun MyScreen() {
    val coroutineScope = rememberCoroutineScope()
 
    Button(onClick = {
        coroutineScope.launch {
            // 按钮点击触发的异步操作
            doSomething()
        }
    })
}

8. 与 Java 并发工具的对应关系速查

你要做的事Java 方式Kotlin 协程方式
执行异步任务executor.submit(runnable)scope.launch { }
获取异步结果executor.submit(callable)scope.async { }.await()
线程池Executors.newFixedThreadPool()Dispatchers.IO / Dispatchers.Default
回调转异步CompletableFuturesuspendCancellableCoroutine
超时控制future.get(timeout, unit)withTimeout(time) { }
组合结果CompletableFuture.thenCombine()coroutineScope { val a = async { }; val b = async { }; a.await() + b.await() }
任务取消future.cancel() (但无法取消正在执行的任务)scope.cancel() (协作式取消,可响应中断)

快速练习

以下 Java 代码用 CompletableFuture 加载两个资源然后合并。请思考如何用 Kotlin 协程改写:

// Java
CompletableFuture<Model> modelFuture =
    CompletableFuture.supplyAsync(() -> loadModel(), executor);
CompletableFuture<Texture> textureFuture =
    CompletableFuture.supplyAsync(() -> loadTexture(), executor);
 
modelFuture.thenCombine(textureFuture, (model, texture) -> {
    return createScene(model, texture);
}).join();
点击查看答案
// Kotlin 协程
coroutineScope {
    val model = async(Dispatchers.IO) { loadModel() }
    val texture = async(Dispatchers.IO) { loadTexture() }
    createScene(model.await(), texture.await()) // 自动等待两个完成
}
 
// 更接近 PICO Demo 风格的方式:
suspend fun loadScene() = withContext(Dispatchers.IO) {
    val modelDeferred = async { loadModel() }
    val textureDeferred = async { loadTexture() }
    createScene(modelDeferred.await(), textureDeferred.await())
}

INFO

参考资料

有疑问? 随时问我,我会用 Java 并发的概念给你类比讲解。


上一课: 第1课:Kotlin for Java Developers | 下一课: 第3课:Kotlin 集合与函数式编程