零基础学会 Kotlin Coroutines 异步编程 – wiki基地


零基础启程:深入理解 Kotlin Coroutines 异步编程

前言:为什么需要异步编程?协程登场!

在软件开发的世界里,我们经常会遇到需要执行耗时操作的场景:

  • 网络请求: 从服务器获取数据可能需要几百毫秒甚至几秒。
  • 文件读写: 读取或写入大文件。
  • 数据库操作: 查询或更新大量数据。
  • 复杂的计算: 执行需要大量 CPU 资源的计算任务。

在传统的同步编程模式下,当一个任务执行时,程序会一直等待直到任务完成,然后才能继续执行下一行代码。这在图形用户界面 (GUI) 应用(如 Android)中会带来严重的问题:如果我们在主线程(也称 UI 线程)上执行一个耗时操作,UI 就会被“冻结”,用户无法进行任何交互,直到操作完成。这显然是不可接受的。

为了解决这个问题,我们引入了异步编程的概念。异步编程允许程序在执行一个耗时任务的同时,继续执行其他任务。当耗时任务完成后,它会通过某种机制(如回调、事件、线程间通信)通知程序,然后程序可以处理任务的结果。

历史上,实现异步编程有多种方式:

  1. 多线程 (Threads): 创建新的线程来执行耗时任务。然而,线程是操作系统级别的资源,创建和管理线程的开销较大,数量过多会导致系统资源耗尽。线程间通信和同步(锁)也比较复杂,容易引发死锁等问题。
  2. 回调 (Callbacks): 将一个函数(回调函数)作为参数传递给耗时操作。当操作完成时,会调用这个回调函数。这解决了主线程阻塞的问题,但对于多个连续的异步操作,会导致“回调地狱”(Callback Hell),代码层层嵌套,难以阅读和维护。
  3. Future/Promise: 表示一个异步操作的未来结果。可以通过链式调用处理结果或错误,比回调有所改进,但链式调用依然可能变得复杂。
  4. 响应式编程 (Reactive Programming,如 RxJava): 提供强大的数据流和操作符,可以优雅地处理异步事件序列。学习曲线相对较陡峭,概念较多。

Kotlin Coroutines (协程) 是 Kotlin 官方推荐的异步编程解决方案。它在 JVM、Android、JavaScript 和 Native 平台上都可用。协程提供了一种更简洁、更直观的方式来编写异步代码,使得异步代码看起来像是同步代码一样。协程被誉为“轻量级线程”,因为它在用户空间而非操作系统空间调度,创建和切换的开销远小于线程。

对于零基础的你来说,理解协程的核心概念是迈出第一步的关键。本文将带你一步步了解协程是什么、如何工作以及如何使用它来编写高效、易读的异步代码。

第一步:理解核心概念 – 什么是协程?

最简单来说,一个协程就像是一个可以被暂停和恢复的计算任务。

想象一个厨师在做饭:

  • 同步方式: 厨师开始烧水,就站在炉子前一直等着,直到水烧开,才去做其他事情。
  • 异步(使用线程): 厨师让一个助手去烧水(创建一个新线程),自己同时去切菜。助手烧好水后,会通知厨师(线程间通信)。
  • 异步(使用协程): 厨师开始烧水,设置一个计时器或标记(协程暂停)。然后,他去做其他事情(协程让出 CPU)。当计时器响了或者水烧开了(耗时操作完成),厨师会回来看水(协程恢复),然后继续做后续步骤。这个厨师可以同时“照看”很多个这样的“暂停中”的任务。

协程最大的特点在于其可暂停性 (Suspendable)。与线程不同,协程不会阻塞它运行的线程。当一个协程遇到一个耗时操作(例如网络请求),它可以暂停自身的执行,让出它所在的线程去执行其他协程或任务。当耗时操作完成后,协程可以恢复执行,就像从未中断过一样。

这种暂停和恢复的能力是由 Kotlin 编译器和协程库共同实现的,它使得我们可以用顺序、同步风格的代码来表达异步逻辑。

第二步:掌握基础 – suspend 函数

协程的核心是 suspend 函数。

suspend 是一个修饰符,它标记了一个函数可以暂停并在之后恢复

kotlin
suspend fun fetchDataFromNetwork(): String {
// 模拟一个耗时的网络请求
println("开始获取数据...")
kotlinx.coroutines.delay(2000) // 暂停协程,不阻塞线程
println("数据获取完成")
return "这是从网络获取的数据"
}

suspend 函数有几个重要的特性:

  1. 只能在协程或另一个 suspend 函数中调用。 你不能直接从一个普通的非 suspend 函数中调用一个 suspend 函数。
  2. 不一定涉及到多线程。 suspend 只是表示函数可以在执行过程中暂停,具体在哪个线程上执行取决于协程的上下文和调度器。

kotlinx.coroutines.delay(milliseconds) 是一个常用的 suspend 函数。它会暂停当前协程的执行,等待指定的毫秒数,但不阻塞底层的线程。这与 Thread.sleep() 不同,Thread.sleep() 会阻塞整个线程。

为什么需要 suspend 修饰符?

编译器需要知道哪些函数可能会“卡住”并暂停协程。suspend 关键字就是告诉编译器:“嘿,这个函数可能会暂停,调用它的时候需要特殊处理。” 编译器会为 suspend 函数生成额外的代码,用于保存和恢复协程的状态。

第三步:启动协程 – 协程构建器 (Coroutine Builders)

我们已经知道 suspend 函数只能在协程或另一个 suspend 函数中调用。那么,如何启动第一个协程呢?这就需要用到协程构建器。

协程构建器是在一个协程作用域 (Coroutine Scope) 中启动一个新的协程的函数。最常用的构建器有:

  1. launch:

    • 用于启动一个“即发即弃 (fire-and-forget)”的协程。
    • 它启动一个新的协程,并在后台执行任务,不返回结果
    • 它返回一个 Job 对象,可以用来管理协程的生命周期(例如取消)。
    • 通常用于执行那些主要目的是副作用(如更新 UI、保存数据)的任务。

    “`kotlin
    import kotlinx.coroutines. // 需要导入 kotlinx.coroutines.

    fun main() = runBlocking { // runBlocking 是另一个构建器,这里用于在主函数中启动协程
    println(“主程序开始”)

    launch { // 在当前协程作用域中启动一个新协程
        println("协程1:开始执行")
        delay(1000) // 暂停 1 秒
        println("协程1:执行完毕")
    }
    
    println("主程序继续执行...")
    // launch 启动的协程在后台运行,主程序不会等待它完成
    // runBlocking 会等待其内部启动的所有协程完成
    

    }
    **输出可能为:**
    主程序开始
    协程1:开始执行
    主程序继续执行…
    协程1:执行完毕
    ``
    注意 "主程序继续执行..." 在 "协程1:开始执行" 之后几乎立即打印,这证明
    launch` 是非阻塞的。

  2. async:

    • 用于启动一个需要返回结果的协程。
    • 它也启动一个新的协程,并在后台执行任务。
    • 它返回一个 Deferred<T> 对象,T 是协程最终会返回的结果类型。Deferred 是一个轻量级的 Future。
    • 可以通过调用 deferred.await() 方法来获取协程的结果。await() 是一个 suspend 函数,它会暂停当前协程,直到 Deferred 的结果可用。

    “`kotlin
    import kotlinx.coroutines.*

    fun main() = runBlocking {
    println(“主程序开始”)

    val deferredResult = async { // 在当前协程作用域中启动一个新协程,期望一个结果
        println("协程2:开始计算")
        delay(1500) // 暂停 1.5 秒
        println("协程2:计算完毕")
        42 // 返回结果 42
    }
    
    println("主程序继续执行...")
    
    // async 启动的协程也在后台运行
    val result = deferredResult.await() // 暂停当前协程,等待协程2的结果
    
    println("主程序:获取到协程2的结果 -> $result")
    

    }
    **输出可能为:**
    主程序开始
    协程2:开始计算
    主程序继续执行…
    协程2:计算完毕
    主程序:获取到协程2的结果 -> 42
    ``
    注意 "主程序继续执行..." 立即打印,但 "主程序:获取到协程2的结果" 在等待了 1.5 秒后才打印,因为它调用了
    await()`。

  3. runBlocking:

    • 主要用于连接非协程世界和协程世界,例如在 main 函数或单元测试中启动一个协程。
    • 它会阻塞调用它的线程,直到其内部启动的所有协程都执行完毕。
    • 不应该在主 UI 线程中使用 runBlocking,因为它会冻结 UI。
    • 我们在上面的例子中使用了 runBlocking,因为它允许我们在标准的 main 函数中演示协程。

    kotlin
    // 已经在上面的例子中展示了
    fun main() = runBlocking {
    // 这里的代码运行在一个协程中,并且会阻塞 main 线程直到这个协程完成
    }

总结构建器:

  • launch: 启动一个协程,不返回结果,得到 Job。用于执行任务。
  • async: 启动一个协程,期望返回结果,得到 Deferred。用于并行计算并获取结果。
  • runBlocking: 启动一个协程并阻塞当前线程直到协程完成。主要用于桥接普通代码和协程代码。

第四步:管理协程生命周期 – 协程作用域 (Coroutine Scope) 和 Job

你可能已经注意到,协程构建器 (launch, async) 都需要在某个“作用域”内调用。这个作用域就是 CoroutineScope

什么是 CoroutineScope?

CoroutineScope 负责跟踪它所创建的协程。它是一个接口,定义了 coroutineContext 属性,这个上下文包含了协程的重要信息(我们稍后会讲)。

为什么需要作用域?

作用域是实现结构化并发 (Structured Concurrency) 的关键。结构化并发是一种编程模式,它确保协程的生命周期被限制在一个特定的作用域内。当这个作用域结束或被取消时,所有在这个作用域内启动的协程也会被自动取消。

这解决了协程泄露的问题。想象一下,如果启动了一个协程去下载文件,但用户突然关闭了屏幕(UI 容器被销毁)。如果没有作用域管理,下载协程可能会继续运行,浪费资源,甚至在下载完成后尝试更新一个不存在的 UI 元素导致崩溃。使用作用域,当 UI 容器被销毁时,相关的协程作用域也会被取消,从而自动取消正在运行的下载协程。

常见的 CoroutineScope:

  • GlobalScope: 一个全局的作用域,生命周期与应用程序一致。强烈不推荐在大多数情况下使用 GlobalScope.launch { ... }GlobalScope.async { ... },因为它违反了结构化并发原则,你启动的协程无法被轻易追踪和取消,容易导致泄露。
  • CoroutineScope(context): 你可以创建自己的作用域,通常通过组合一个 Job 和一个 Dispatcher 来实现。
  • 构建器创建的作用域: runBlocking, launch, async 等构建器自身会创建一个子协程作用域。在其 lambda 内部,this 就代表了这个子作用域。
  • 由库提供的特定作用域: 例如在 Android 开发中,lifecycleScopeviewModelScope 是由 Jetpack 库提供的,它们绑定到特定的生命周期(如 Activity 或 ViewModel),并在生命周期结束时自动取消协程。

Job

当我们使用 launch 启动一个协程时,它会返回一个 Job 对象。

Job 是一个句柄,代表着一个协程的生命周期。你可以使用 Job 来:

  • 等待协程完成: 调用 job.join() (一个 suspend 函数)。
  • 取消协程: 调用 job.cancel()
  • 检查协程状态: job.isActive, job.isCompleted, job.isCancelled.

async 返回的 Deferred 继承自 Job,所以你也可以对 Deferred 调用 cancel()join()

“`kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
println(“主程序开始”)

val job = launch { // launch 返回一个 Job
    try {
        repeat(1000) { i ->
            println("Job: 我还在运行 $i ...")
            delay(500) // 模拟工作并提供取消点
        }
    } catch (e: CancellationException) {
        println("Job: 我被取消了!")
    } finally {
         println("Job: 我结束了。")
    }
}

delay(1300) // 等待一小段时间
println("主程序:时间到了,我要取消 Job!")
job.cancelAndJoin() // 取消 Job 并等待它完成取消

println("主程序:Job 已被取消并完成。")

}
“`

输出可能为:
主程序开始
Job: 我还在运行 0 ...
Job: 我还在运行 1 ...
Job: 我还在运行 2 ...
主程序:时间到了,我要取消 Job!
Job: 我被取消了!
Job: 我结束了。
主程序:Job 已被取消并完成。

这个例子展示了如何通过 Job 来取消一个协程。注意,协程的取消是协作式 (cooperative) 的。这意味着协程必须主动检查取消状态并停止执行。Kotlin 协程库中的所有 suspend 函数(如 delay, yield, 文件 IO 操作)都是可取消的,它们会在被调用时检查协程的取消状态。如果你在一个没有调用任何可取消的 suspend 函数的长计算循环中,协程将不会自动取消,你需要手动检查 isActive 属性或调用 yield()

第五步:控制协程执行在哪里 – 协程上下文 (Coroutine Context) 和调度器 (Dispatcher)

每个协程都有一个与之关联的 CoroutineContext。上下文是一组元素的集合,它定义了协程的行为方式。其中最重要的元素之一是 CoroutineDispatcher

CoroutineDispatcher 决定了协程在哪个线程或线程池上执行。你可以把它想象成一个“工作车间”。

常见的调度器有:

  1. Dispatchers.Main:

    • 专用于 UI 线程。在 Android 中,它就是主线程。
    • 只能在这个调度器上执行更新 UI 的操作。
    • 如果在没有 UI 环境(如 JVM 命令行应用)中使用,可能会抛出异常或行为异常。
    • 通常用于: 启动协程来执行 UI 更新任务,或者在其他调度器上完成耗时操作后切换回主线程更新 UI。
  2. Dispatchers.IO:

    • 专用于执行 I/O 密集型任务,如网络请求、文件读写、数据库操作。
    • 它使用一个共享的按需创建的线程池,效率高,适合处理大量并发的阻塞 I/O 操作。
    • 通常用于: 进行网络调用、访问数据库或文件系统。
  3. Dispatchers.Default:

    • 专用于执行 CPU 密集型任务。
    • 它使用一个固定大小的线程池,默认线程数等于 CPU 核数。
    • 通常用于: 执行复杂的计算、排序、解析大型 JSON 等。
  4. Dispatchers.Unconfined:

    • 特殊调度器。它在调用者线程中启动协程,但协程在第一次暂停后,将在恢复它的线程中继续执行。
    • 通常不建议使用,因为它不限制协程在特定线程池中执行,可能导致不可预测的行为和资源消耗。只在极少数特殊场景下使用,且需要非常清楚其工作原理。

如何指定调度器?

你可以在协程构建器中通过参数指定调度器:

“`kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
launch(Dispatchers.Default) {
println(“在 Dispatchers.Default 上运行,线程:${Thread.currentThread().name}”)
}

launch(Dispatchers.IO) {
    println("在 Dispatchers.IO 上运行,线程:${Thread.currentThread().name}")
}

// runBlocking 默认在调用它的线程上运行
println("在 runBlocking 的线程上运行,线程:${Thread.currentThread().name}")

}
“`

输出示例 (线程名会有所不同):
在 runBlocking 的线程上运行,线程:main
在 Dispatchers.Default 上运行,线程:DefaultDispatcher-worker-1
在 Dispatchers.IO 上运行,线程:DefaultDispatcher-worker-2

切换调度器 – withContext

很多时候,一个任务可能需要在不同的调度器之间切换。例如,在 IO 调度器上执行网络请求,然后在 Main 调度器上更新 UI。withContext 函数就是为此设计的。

withContext(context) 是一个 suspend 函数。它会切换到指定的上下文(通常是不同的调度器),执行在其 lambda 内部的代码块,然后恢复到原来的上下文。

“`kotlin
import kotlinx.coroutines.*

suspend fun performNetworkRequest(): String =
withContext(Dispatchers.IO) { // 切换到 IO 调度器
println(“在 performNetworkRequest 内部,线程:${Thread.currentThread().name}”)
delay(2000) // 模拟网络请求
“网络数据”
}

fun main() = runBlocking {
println(“在主 runBlocking 协程中,线程:${Thread.currentThread().name}”)

val result = performNetworkRequest() // 调用 suspend 函数,它会在内部切换调度器

// withContext 块执行完毕后,自动切换回原来的调度器 (这里是 runBlocking 的线程)
println("网络请求完成,结果:$result,现在回到线程:${Thread.currentThread().name}")

// 在 Android 中,这里通常会切换到 Dispatchers.Main 来更新 UI
// withContext(Dispatchers.Main) {
//     updateUI(result)
// }

}
“`

输出示例:
在主 runBlocking 协程中,线程:main
在 performNetworkRequest 内部,线程:DefaultDispatcher-worker-1
网络请求完成,结果:网络数据,现在回到线程:main

withContext 是一个非常强大和常用的函数,它让你可以轻松地在不同的执行环境之间切换,同时保持代码的顺序性。

第六步:处理多个异步操作

在实际应用中,我们经常需要同时或顺序地执行多个异步任务。

顺序执行:

如果一个任务依赖于前一个任务的结果,或者它们必须按顺序发生,只需按顺序调用 suspend 函数即可:

kotlin
suspend fun fetchUserData(): String {
val user = fetchUser() // suspend 函数 1
val messages = fetchMessages(user.id) // suspend 函数 2, 依赖于 user
return "用户 ${user.name} 的消息:${messages.joinToString(", ")}"
}

由于 fetchUserfetchMessages 都是 suspend 函数,当调用 fetchUser 时,协程会暂停直到它完成并返回结果,然后才继续调用 fetchMessages。这看起来就像同步代码,但底层是非阻塞的。

并行执行:

如果两个任务之间没有依赖关系,并且可以同时执行以节省时间,可以使用 launchasync

  • 并行执行,不关心结果是否立即返回 (Fire-and-forget): 使用 launch

    kotlin
    coroutineScope { // 创建一个结构化的并发作用域
    launch { // 任务 A
    delay(1000)
    println("任务 A 完成")
    }
    launch { // 任务 B
    delay(1500)
    println("任务 B 完成")
    }
    println("两个任务都已启动")
    } // coroutineScope 会等待所有子协程完成
    println("coroutineScope 结束")

    输出可能为:
    两个任务都已启动
    任务 A 完成
    任务 B 完成
    coroutineScope 结束

    coroutineScope { ... } 是一个 suspend 函数,它创建一个新的作用域,并会暂停当前协程,直到在其内部启动的所有子协程都完成。这是一个实现结构化并发的好方法。

  • 并行执行,需要等待结果: 使用 asyncawait

    “`kotlin
    import kotlinx.coroutines.*
    import kotlin.system.measureTimeMillis

    suspend fun fetchUserData(): String {
    delay(1000)
    return “用户数据”
    }

    suspend fun fetchPreferences(): String {
    delay(1200)
    return “用户偏好”
    }

    fun main() = runBlocking {
    val time = measureTimeMillis {
    val userDataDeferred = async { fetchUserData() } // 启动第一个并行任务
    val preferencesDeferred = async { fetchPreferences() } // 启动第二个并行任务

        // 同时等待两个任务的结果
        val userData = userDataDeferred.await()
        val preferences = preferencesDeferred.await()
    
        println("同时获取:$userData 和 $preferences")
    }
    println("总耗时:$time ms")
    

    }
    ``
    如果这两个函数是顺序调用的,总耗时会是
    1000 + 1200 = 2200 ms左右。使用async/await并行执行,总耗时会接近于最长的那个任务的时间1200 ms` 左右(加上协程启动和切换的开销)。

    注意:上面的例子是在 runBlocking 作用域内使用 async。在实际应用中,你会在 lifecycleScopeviewModelScope 等作用域内使用 async

第七步:协程的取消 (Cancellation)

前面我们提到了协程的取消是协作式的。了解如何处理取消非常重要。

  1. 取消一个 Job: 调用 job.cancel()job.cancelAndJoin()
  2. 检测取消状态:

    • 自动: 调用大多数 kotlinx.coroutines 库提供的 suspend 函数(如 delay, yield, withContext, isActive 检查)会在内部检查协程的取消状态,如果已取消则抛出 CancellationException
    • 手动: 在计算密集型循环中,定期检查 isActive 属性:

      kotlin
      launch {
      while (isActive) { // 检查协程是否活跃(未被取消)
      // 执行部分计算任务
      }
      }

      * 手动: 调用 yield() 函数。yield() 会让出当前线程的执行,检查协程的取消状态,并在必要时抛出 CancellationException

  3. 处理取消: 当协程被取消时,它会抛出 CancellationException。这是一个特殊的异常,通常不应该被常规的 try...catch 块捕获并忽略,除非你明确知道自己在做什么。通常,你应该在 finally 块中执行清理操作。

    kotlin
    val job = launch {
    try {
    // 协程体,可能被取消
    delay(5000)
    } catch (e: CancellationException) {
    println("任务被取消了")
    throw e // 重新抛出 CancellationException 是个好习惯,以便父协程知道发生了取消
    } finally {
    // 执行清理操作,无论是否被取消都会执行
    println("执行清理")
    }
    }
    delay(1000)
    job.cancel()

结构化并发通过作用域自动处理取消。当父协程(或作用域)被取消时,它的所有子协程也会被递归地取消。

第八步:协程的异常处理 (Exception Handling)

在协程中处理异常需要特别注意,因为异常的传播方式取决于协程的结构。

  1. launch 的异常传播:

    • 使用 launch 构建器启动的根协程(没有父协程的协程)如果抛出未捕获的异常,该异常通常会传播到父 Job
    • 如果你在 GlobalScope 中启动协程或使用自定义 CoroutineScope 且未指定异常处理器,异常可能会导致应用程序崩溃(在 Android 上)。
    • 为了处理这种情况,可以使用 CoroutineExceptionHandler

    “`kotlin
    import kotlinx.coroutines.*

    fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
    println(“捕获到异常:$exception”)
    }

    val job = launch(handler) { // 在 launch 的上下文中添加异常处理器
        println("启动会抛出异常的协程")
        delay(100)
        throw RuntimeException("这是个错误!")
    }
    
    job.join() // 等待协程完成(或失败)
    println("主程序结束")
    

    }
    输出:
    启动会抛出异常的协程
    捕获到异常:java.lang.RuntimeException: 这是个错误!
    主程序结束
    ``CoroutineExceptionHandler只对那些未通过结构化并发传播的异常起作用,通常用于顶级协程(根协程或由CoroutineScope.launch` 启动的直接子协程)。

  2. async 的异常传播:

    • 使用 async 构建器启动的协程,异常会存储在返回的 Deferred 对象中。
    • 异常会在调用 await() 时被重新抛出。
    • 因此,对于 async 启动的协程,应该在调用 await() 的地方使用标准的 try...catch 块来捕获异常。

    “`kotlin
    import kotlinx.coroutines.*

    fun main() = runBlocking {
    val deferred = async {
    println(“启动会抛出异常的 async 协程”)
    delay(100)
    throw IllegalArgumentException(“async 里的错误!”)
    “结果” // 这行不会被执行
    }

    try {
        val result = deferred.await() // 在 await() 时会抛出异常
        println("获取到结果:$result")
    } catch (e: IllegalArgumentException) {
        println("成功捕获 async 里的异常:$e")
    }
    println("主程序结束")
    

    }
    输出:
    启动会抛出异常的 async 协程
    成功捕获 async 里的异常:java.lang.IllegalArgumentException: async 里的错误!
    主程序结束
    “`

注意: 如果一个父协程使用 coroutineScope 创建了一个作用域,并且其中一个子协程(无论是 launch 还是 async 启动的)失败了,那么该异常会立即导致该 coroutineScope 自身失败,并取消所有其他子协程。这是结构化并发中异常处理的一部分。

第九步:协程与 UI 编程 (以 Android 为例)

虽然本文不深入特定平台,但了解协程如何在 UI 环境工作非常重要。

在 Android 中,通常使用 Jetpack Architecture Components 提供的协程集成库,它们提供了绑定到组件生命周期的 CoroutineScope

  • lifecycleScope: 绑定到 Activity 或 Fragment 的生命周期。
  • viewModelScope: 绑定到 ViewModel 的生命周期。

使用这些作用域启动协程,可以在 Activity/Fragment 销毁或 ViewModel 清除时自动取消协程,防止内存泄漏。

典型 Android 异步操作模式:

“`kotlin
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class MyViewModel : ViewModel() {

fun loadData() {
    viewModelScope.launch { // 在 viewModelScope 中启动协程 (运行在主线程)
        // 可以在这里更新 UI 状态,例如显示加载指示器

        val data = withContext(Dispatchers.IO) { // 切换到 IO 调度器执行耗时操作
            // 模拟网络请求
            delay(2000)
            "从服务器获取的数据"
        }

        // withContext 结束后,自动切换回 viewModelScope 的上下文 (主线程)
        // 可以在这里更新 UI
        println("数据加载完成:$data") // 在 Android 中会是更新 LiveData/StateFlow 等
    }
}

}
“`

这个模式非常常见:在 UI 相关的协程作用域(如 viewModelScope)中启动协程,使用 withContext(Dispatchers.IO) 执行网络/数据库等耗时操作,然后在操作完成后自动回到原来的线程(主线程)更新 UI。

第十步:下一步 – 深入学习 Flow 和 Channels

掌握了 suspend 函数、构建器 (launch, async, runBlocking)、CoroutineScopeJobDispatcher、取消和异常处理,你就已经掌握了协程的基础。

协程库还提供了更高级的概念来处理异步数据流和并发通信:

  • Flow: 用于处理异步数据流,类似于响应式编程中的 Observable。它允许你表示一个可以发出多个值随时间变化的异步计算。非常适合处理数据库更新、实时网络数据等。
  • Channels: 用于协程之间的通信,可以在不同协程之间安全地传递数据。类似于并发编程中的阻塞队列。

这些是协程的进阶主题,可以在你掌握了基础之后深入学习。

总结与实践

Kotlin Coroutines 提供了一种强大而优雅的方式来处理异步编程。通过将异步代码写成看似顺序执行的同步代码,它大大提高了代码的可读性和可维护性。

学习协程的关键在于理解其核心概念:

  • suspend 函数的可暂停/恢复性。
  • CoroutineScope 实现结构化并发,管理协程生命周期。
  • 协程构建器 (launch, async) 用于启动协程。
  • Job 用于管理协程的生命周期。
  • CoroutineDispatcher 控制协程执行的线程/线程池。
  • withContext 用于方便地切换调度器。
  • 协程取消的协作性。
  • launchasync 在异常处理上的区别。

从零开始学习协程,最好的方法是:

  1. 安装 Kotlin 和 Coroutines 库。
  2. 从简单的例子开始。 尝试使用 runBlockinglaunch/async,加入 delay 来模拟耗时操作,观察输出顺序。
  3. 实验 Dispatchers。 尝试在不同的调度器上运行代码,打印当前线程名称,理解它们的作用。
  4. 练习取消。 使用 Job.cancel(),观察协程如何响应。
  5. 练习异常处理。 尝试在 launchasync 中抛出异常,使用 CoroutineExceptionHandlertry...catch 来处理。
  6. 在实际项目中应用。 如果你在进行 Android 开发,尝试将传统的异步代码(如 Callbacks 或 AsyncTask)迁移到协程。

记住,实践是掌握任何新技能的最好方法。协程初看可能有些抽象,但一旦你理解了核心概念,并动手实践,你会发现它们是编写现代异步应用的极其强大的工具。

祝你在协程的学习之旅中一切顺利!

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部