协程

协程是 Kotlin 处理并发和异步编程的核心方案。它让你用接近顺序代码的方式编写异步逻辑,同时避免为大量并发任务创建大量操作系统线程。

对 Java 开发者来说,可以先把协程理解为“可挂起的计算”:它不是线程,但会在线程上运行;它可以暂停并释放线程,稍后再恢复执行。

为什么不是直接用线程

Java 线程由操作系统管理,功能强,但成本较高。大量线程会带来:

  • 栈内存占用。
  • 上下文切换成本。
  • 调度压力。
  • 难以管理的生命周期。

协程更轻量。协程挂起时不会阻塞线程,线程可以继续执行其他协程。因此协程适合大量 IO 等待、细粒度并发和 UI/服务端异步流程。

依赖

语言本身提供 suspend,但大多数协程能力来自 kotlinx.coroutines

Gradle Kotlin DSL:

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
}

版本应按项目 Kotlin 版本和官方发布说明更新。

suspend 函数

suspend fun loadUser(id: Long): User {
    delay(100)
    return User(id, "Ada")
}

suspend 表示函数可以挂起和恢复。挂起不是阻塞线程。

只能从下面位置调用 suspend 函数:

  • 另一个 suspend 函数。
  • 协程内部。
  • 特殊桥接函数,例如 runBlocking

示例:

suspend fun main() {
    val user = loadUser(1)
    println(user)
}

runBlocking

runBlocking 会创建协程作用域,并阻塞当前线程直到内部协程完成:

fun main() = runBlocking {
    val user = loadUser(1)
    println(user)
}

它适合:

  • main 函数桥接。
  • 测试代码。
  • 无法修改为 suspend 的边界。

不适合:

  • Web 请求处理线程中随意包裹异步代码。
  • Android 主线程。
  • 已经在协程中的代码。

如果你在协程里还想切换上下文,使用 withContext

CoroutineScope 与结构化并发

协程必须运行在 CoroutineScope 中。结构化并发的核心思想是:相关协程形成父子层级,父协程会等待子协程完成;父协程取消时,子协程也会取消。

suspend fun loadDashboard(): Dashboard = coroutineScope {
    val user = async { loadUser() }
    val orders = async { loadOrders() }

    Dashboard(user.await(), orders.await())
}

coroutineScope 会等待内部所有子协程结束才返回。

不要随意创建脱离生命周期的全局协程。Java 中常见“随手 new Thread/start”的问题,在协程里对应“随手 GlobalScope.launch”。

launch 与 async

launch 启动一个不直接返回结果的协程,返回 Job

val job = launch {
    sendLog()
}

job.join()

适合 fire-and-wait 或后台任务。

async 启动一个会产生结果的协程,返回 Deferred<T>

val user = async { loadUser() }
val orders = async { loadOrders() }

Dashboard(user.await(), orders.await())

适合并发计算结果。不要用 async 后不 await(),那通常说明你应该用 launch

默认顺序执行

suspend 函数默认仍然顺序执行:

val user = loadUser()
val orders = loadOrders()

这不会自动并发。需要并发时显式使用 async

val user = async { loadUser() }
val orders = async { loadOrders() }

Dashboard(user.await(), orders.await())

这种显式性很重要:并发会影响异常传播、取消和资源占用,不应该悄悄发生。

Dispatcher

Dispatcher 决定协程在哪些线程上执行。

常见 dispatcher:

  • Dispatchers.Default:CPU 密集型任务,例如计算、解析、排序。
  • Dispatchers.IO:阻塞 IO,例如文件、数据库、旧 HTTP 客户端。
  • Dispatchers.Main:UI 主线程,Android、桌面 UI 等场景。

切换上下文:

suspend fun readConfig(): Config =
    withContext(Dispatchers.IO) {
        file.readText().toConfig()
    }

不要把阻塞 IO 放到 Default,也不要在 Main 上执行耗时操作。

取消

协程取消是协作式的。常见挂起函数会检查取消并抛出 CancellationException

val job = launch {
    repeat(1000) {
        delay(100)
        println(it)
    }
}

delay(500)
job.cancel()

CPU 密集循环需要主动检查:

while (isActive) {
    doChunk()
}

不要吞掉 CancellationException,否则取消会失效。

异常传播

结构化并发中,子协程失败通常会取消父作用域和兄弟协程。launch 中未捕获异常会作为协程异常处理;async 的异常通常在 await() 时暴露。

val deferred = async {
    error("failed")
}

try {
    deferred.await()
} catch (e: IllegalStateException) {
    println(e.message)
}

异常处理要结合作用域设计,不要只在最内层 try-catch 后吞掉异常。

Flow:异步值流

suspend 函数返回一个值;Flow 表达一段时间内产生多个值。

fun loadPages(): Flow<String> = flow {
    emit("Page 1")
    emit("Page 2")
    emit("Page 3")
}

suspend fun main() {
    loadPages()
        .map { it.uppercase() }
        .collect { println(it) }
}

冷 Flow 默认惰性执行:只有 collect() 时才开始生产值。每个 collector 通常会触发一次新的执行。

冷 Flow 与热 Flow

冷 Flow:

  • 类似异步版 Sequence。
  • collect 时才执行。
  • 每个 collector 独立执行。
  • 适合网络请求、数据库查询、分页加载。

热 Flow:

  • 即使没有 collector 也可能继续发射。
  • 多个 collector 共享同一数据源。
  • SharedFlow 适合事件广播。
  • StateFlow 适合状态持有,例如 UI state。
private val _state = MutableStateFlow(UserState.Loading)
val state: StateFlow<UserState> = _state.asStateFlow()

Channel 简述

Channel 用于协程之间发送和接收值,每个值通常被一个接收者消费:

val channel = Channel<Int>()

launch {
    channel.send(1)
    channel.close()
}

for (value in channel) {
    println(value)
}

如果你想表达持续状态,优先考虑 StateFlow;如果想表达广播事件,考虑 SharedFlow;如果是生产者消费者队列,Channel 更自然。

Java 对比

Java 常见异步模型:

  • Thread
  • ExecutorService
  • Future
  • CompletableFuture
  • Reactive Streams / Reactor / RxJava

Kotlin 协程与它们的核心差异:

  • suspend 保持顺序代码风格。
  • 用结构化并发管理生命周期。
  • Deferred 表达异步结果。
  • 用 Flow 表达异步流。
  • 取消和异常传播是设计的一部分。

从 Java 迁移时,不要把每个 CompletableFuture 机械翻译成 async。先看调用关系:如果只是顺序等待,suspend 函数就够;只有独立任务需要并发时才用 async

实践建议

  • suspend 函数默认顺序执行;并发要显式写 asynclaunch
  • 遵守结构化并发,避免 GlobalScope
  • 阻塞 IO 放到 Dispatchers.IO
  • CPU 密集任务放到 Dispatchers.Default
  • 不要在协程中吞掉 CancellationException
  • 返回单个异步结果用 suspend 函数,返回多个异步值用 Flow。
  • Android、服务端框架和测试都有自己的协程作用域,优先使用框架提供的 scope。

参考

  • 官方协程概览:https://kotlinlang.org/docs/coroutines-overview.html
  • 官方协程基础:https://kotlinlang.org/docs/coroutines-basics.html
  • 官方挂起函数组合:https://kotlinlang.org/docs/composing-suspending-functions.html
  • 官方 Flow 文档:https://kotlinlang.org/docs/coroutines-flow.html