Kotlin 协程

协程(Coroutine)是 Kotlin 最强大的特性之一——用同步的写法做异步的事。它是 Android 异步编程的事实标准,也广泛用于服务端(Spring、Ktor)。协程极轻量(一个应用跑十万个协程没问题),无回调地狱,学习曲线比 RxJava 平缓得多。本篇系统讲。

1. 协程基础

用协程前先加依赖:org.jetbrains.kotlinx:kotlinx-coroutines-core。最小化示例:

// 协程:轻量级异步方案,用同步的写法做异步的事
// 需要先添加依赖:org.jetbrains.kotlinx:kotlinx-coroutines-core

import kotlinx.coroutines.*

fun main() = runBlocking {        // runBlocking 启动顶层协程作用域
    launch {                       // 启动一个新协程
        delay(1000)                // 非阻塞延迟(挂起 1 秒)
        println("一秒后")
    }

    println("立即打印")
}
// 输出:
// 立即打印
// 一秒后
// (然后 main 结束)

核心概念:runBlocking 启动顶层作用域(阻塞当前线程,常用于 main 函数和测试);launch 启动一个新协程(返回 Job,不返回结果);delay非阻塞延迟(挂起协程,不阻塞线程)。

2. suspend 挂起函数(关键概念)

suspend 是协程最重要的关键字。suspend 函数只能在协程或其他 suspend 函数里调用:

// suspend 函数:挂起函数,只能在协程里调用
// 【不会阻塞线程】,而是【挂起协程】,释放线程给其他协程用

suspend fun doWork(): String {
    delay(500)                    // 挂起 500ms,不阻塞
    return "结果"
}

suspend fun fetchUser(id: Int): User {
    delay(300)                    // 模拟网络请求
    return User(id, "用户$id")
}

// suspend 函数可以调用其他 suspend 函数
suspend fun loadData(): User {
    val user = fetchUser(1)       // 调用其他 suspend
    println("加载完成: $user")
    return user
}

// 普通 fun 不能调用 suspend 函数,会编译错误
// fun main() {
//     delay(100)                  // 错误!delay 是 suspend
// }
//
// 必须用 suspend fun 或在协程里调用

关键理解:suspend 不是阻塞——它会挂起协程,释放当前线程给其他协程用,等条件满足后恢复。这是协程比线程高效的核心机制。

底层原理:每个 suspend 函数会被编译成状态机,在挂起点保存局部变量,恢复时还原。这种"代码看起来同步、底层完全异步"的设计,叫"continuation-passing style 变换"。

3. launch vs async

启动协程的两个主要 builder:

// launch:启动一个【不返回结果】的协程(类似 fire-and-forget)
// async:启动一个【返回结果】的协程,返回 Deferred<T>

import kotlinx.coroutines.*

fun main() = runBlocking {
    // launch 的"任务"
    val job1 = launch {
        delay(500)
        println("任务1完成")
    }

    // async 的"任务",返回 Deferred
    val deferred = async {
        delay(300)
        42                        // 返回值
    }

    println("等待 async 结果...")
    val result = deferred.await()  // 等待 async 完成
    println("async 结果: $result")

    job1.join()                    // 等待 job1 完成
}
// 输出:
// 等待 async 结果...
// 任务1完成
// async 结果: 42

// await() 是 suspend,只能在协程里调用

经验:优先用 launch,只在需要返回值时用 async。async 的异常只在 await 时抛出,容易遗漏处理。

4. 并行 vs 串行(协程的杀手锏)

多个独立的异步任务用 async 并行,总耗时 = max(各任务)而非累加:

// 并行执行多个任务
import kotlinx.coroutines.*

fun main() = runBlocking {
    // 串行:总耗时 = 任务A + 任务B + 任务C
    val serialTime = measureTimeMillis {
        val a = fetchData("A")     // 100ms
        val b = fetchData("B")     // 200ms
        val c = fetchData("C")     // 150ms
    }
    println("串行: ${serialTime}ms")  // ~450ms

    // 并行:总耗时 = max(A, B, C)
    val parallelTime = measureTimeMillis {
        val a = async { fetchData("A") }
        val b = async { fetchData("B") }
        val c = async { fetchData("C") }
        println(a.await() + b.await() + c.await())
    }
    println("并行: ${parallelTime}ms")  // ~200ms
}

suspend fun fetchData(name: String): String {
    delay(when (name) {
        "A" -> 100
        "B" -> 200
        "C" -> 150
        else -> 0
    })
    return name
}

这就是协程的核心价值——异步并行代码看起来和同步串行代码一样清晰。对比传统的回调写法或 Future 链,这种"看起来同步、实际并行"的代码可读性极强。

5. Dispatchers(调度器)

调度器决定协程在哪个线程跑。Kotlin 内置 4 个:

// Dispatchers:协程调度器,决定协程在哪个线程跑
import kotlinx.coroutines.*

fun main() = runBlocking {
    // Main:主线程(Android UI 线程)
    launch(Dispatchers.Main) {
        println("Main: ${Thread.currentThread().name}")
    }

    // IO:IO 线程池(网络、磁盘、数据库)
    launch(Dispatchers.IO) {
        println("IO: ${Thread.currentThread().name}")
    }

    // Default:CPU 密集型线程池(计算、排序、解析)
    launch(Dispatchers.Default) {
        println("Default: ${Thread.currentThread().name}")
    }

    // Unconfined:不限制,继承调用方线程(不推荐常用)
    launch(Dispatchers.Unconfined) {
        println("Unconfined: ${Thread.currentThread().name}")
    }

    // withContext:切换上下文(常用)
    val result = withContext(Dispatchers.IO) {
        // 这个块在 IO 线程跑
        fetchData("X")
    }
    // 回到原线程
    println("结果: $result")
}

withContext 用于切换上下文:withContext(Dispatchers.IO) { fetchData() } 切到 IO 线程跑 fetchData,完成后自动切回。这是 Android 老套路:withContext(IO) 取数据 → withContext(Main) 更新 UI

6. 协程作用域(structured concurrency)

这是 Kotlin 协程的核心设计——每个协程都属于一个作用域,作用域结束会自动取消所有子协程:

// CoroutineScope:协程作用域,管理协程的生命周期
// 作用域结束时,内部所有协程会自动取消

// 自定义作用域(用工厂方法)
val scope = CoroutineScope(Dispatchers.Main)

scope.launch {
    // 这个协程受 scope 管理
}

// structured concurrency(结构化并发):
// 子协程的失败会传播到父协程,父协程取消会取消所有子协程
class MyService {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    fun startWork() {
        scope.launch { /* ... */ }
    }

    fun destroy() {
        scope.cancel()            // 取消所有子协程
    }
}

// runBlocking 是顶层作用域(用于 main、测试)
// GlobalScope 谨慎用:生命周期和应用一样长,易内存泄漏

结构化并发的好处:

Android 用 lifecycleScope(Jetpack)、viewModelScope,它们的 scope 绑定到生命周期,自动取消。

7. Flow(协程版的数据流)

Flow 是协程版的"流",类似 RxJava 的 Observable 但更简洁。用于异步、按需、流式的数据生产:

// Flow:协程版的"流"(类似 RxJava 的 Observable)
// 用于【异步、按需、流式】的数据
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun numbers(): Flow<Int> = flow {
    for (i in 1..5) {
        delay(300)               // 模拟异步生产
        emit(i)                  // 发射一个值
    }
}

fun main() = runBlocking {
    // 收集 Flow
    numbers().collect { value ->
        println("收到: $value")
    }
    // 输出(每 300ms 一个):
    // 收到: 1
    // 收到: 2
    // ...

    // Flow 的操作符(类似 RxJava)
    val result = numbers()
        .map { it * it }                       // 转换
        .filter { it > 4 }                     // 过滤
        .take(2)                                // 取前 2 个
        .toList()
    println(result)                            // [9, 16]

Flow 是冷流(cold flow)——只有 collect 时才会真正开始生产,且每个 collector 各生产一份。这避免了 RxJava 的某些坑。

常用操作符:mapfiltertakeflatMapLatestdebouncedistinctUntilChanged 等。还有StateFlow(状态)、SharedFlow(事件总线),是 Android 替代 LiveData 的事实标准。

8. 异常处理

// 异常处理:协程异常会传播
import kotlinx.coroutines.*

fun main() = runBlocking {
    // try-catch 可以捕获子协程的异常
    try {
        val job = launch {
            throw RuntimeException("协程出错")
        }
        job.join()
    } catch (e: Exception) {
        println("捕获: ${e.message}")
    }

    // async 的异常只在 await() 时抛出
    val deferred = async {
        delay(100)
        throw RuntimeException("async 出错")
    }
    try {
        deferred.await()
    } catch (e: Exception) {
        println("捕获: ${e.message}")
    }

    // CoroutineExceptionHandler:全局异常处理器(用于 launch)
    val handler = CoroutineExceptionHandler { _, e ->
        println("未处理异常: ${e.message}")
    }
    launch(handler) {
        throw RuntimeException("未捕获")
    }
}

几个关键点:

9. 取消与超时

import kotlinx.coroutines.*

fun main() = runBlocking {
    // 超时
    val result = withTimeoutOrNull(500) {
        repeat(10) {
            delay(200)
            println("tick $it")
        }
        "完成"
    }
    println("结果: $result")              // 结果: null(超时了)

    // 主动取消
    val job = launch {
        repeat(100) { i ->
            delay(100)
            println("工作 $i")
        }
    }
    delay(350)
    job.cancel()                          // 取消
    job.join()                            // 等待真的结束
    println("已取消")

    // 协程代码要"配合"取消:
    // delay() / 协程 API 都会检查取消
    // 但 CPU 密集型代码要手动 yield()
    suspend fun heavyWork() {
        for (i in 1..1_000_000) {
            yield()                        // 主动让出,允许取消
            // 或者 ensureActive() / isActive 检查
        }
    }
}

结构化并发的一个优势:取消是协作式的——协程代码主动检查取消(通过 suspend 函数),可以优雅清理资源(关闭文件、回滚事务)。

10. 实战:并行加载多接口

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.system.measureTimeMillis

data class User(val id: Int, val name: String)
data class Profile(val bio: String)
data class Avatar(val url: String)

suspend fun fetchUser(id: Int): User {
    delay(200)                       // 模拟 API
    return User(id, "用户$id")
}

suspend fun fetchProfile(id: Int): Profile {
    delay(150)
    return Profile("这是用户 $id 的简介")
}

suspend fun fetchAvatar(id: Int): Avatar {
    delay(300)
    return Avatar("https://example.com/$id.png")
}

fun main() = runBlocking {
    val time = measureTimeMillis {
        // 三个接口并行请求
        val userDeferred = async { fetchUser(42) }
        val profileDeferred = async { fetchProfile(42) }
        val avatarDeferred = async { fetchAvatar(42) }

        // 等所有结果(总耗时 ≈ 300ms,而不是 200+150+300=650ms)
        val user = userDeferred.await()
        val profile = profileDeferred.await()
        val avatar = avatarDeferred.await()

        println("User: $user")
        println("Profile: $profile")
        println("Avatar: $avatar")
    }
    println("总耗时: ${time}ms")             // ~300ms
}

这是典型的"并行加载多个独立接口"模式——一个页面的数据来自多个 API,并行请求可以大幅缩短加载时间。如果用传统回调写,代码嵌套三层、难维护;用协程写,看起来和串行代码一样清晰。

11. 协程 vs 线程 vs RxJava

小结

这一章你掌握了 Kotlin 协程的全部核心:suspend 函数、launch/async、Dispatchers 调度器、CoroutineScope 结构化并发、Flow 数据流、异常处理、取消超时。协程是 Kotlin 区别于 Java 的最大杀手锏之一——一旦习惯,你会觉得 Java 的回调/CompletableFuture 都是噪音。

恭喜!你已经完成了整个 Kotlin 16 篇入门系列。从最简单的 val/var 到协程,你已经掌握了 Android 官方语言的全部核心特性。下一步:写一个 Android 小项目、读 Kotlin 源码、深入 Jetpack Compose 或 Ktor 服务端。Kotlin 是一门"实用主义"的语言——它不追求理论上的纯粹,而是把各种语言里最好用的特性整合到一起,让开发者写得爽、出错少。

← 上一篇 Kotlin Lambda 与高阶函数

返回 Kotlin 教程目录

✈️💬