Kotlin 、协程、结构化并发

本文由一名有三年Android开发经验的程序员分享,探讨Kotlin协程的异步操作和并行分解。文章指出,与线程相比,协程创建成本低廉,适合大量创建。在结构化并发的指导下,建议在协程作用域内启动任务,以确保正确处理取消和UI更新。同时,通过示例代码说明了如何避免并发编程中的常见问题,如取消操作的正确处理和并行任务的同步。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

写在前面:一名有三年Android开发经验的女程序员(欢迎大家关注我 ~期待和大家一起交流和学习Android的相关知识)

在 Kotlin 1.1 也就是 2017年初, 首次推出协程作为实验性质的特性开始,我们一直在努力向程序员解释协程的概念,他们过去常常使用线程理解并发,所以我们举的例子和标语是"协程是轻量级线程"。

此外,我们的关键 api 被设计为类似于线程 api,以简化学习曲线。这种类比在小规模例子中很适用,但是它不能帮助解释协程编程风格的转变。

当我们学习使用线程编程时,我们被告知线程是昂贵的资源,不应该到处创建它们。一个优雅的程序通常在启动时创建一个线程池然后使用它们搞些事情。有些环境(尤其是 iOS)甚至说"不赞成使用线程"(即使所有的东西仍然在线程上运行)。它们提供了一个系统内的随时可用的线程池,其中包含可向其提交代码的相应队列。

但是协程的情况不同。它可以非常方便地创建很多你需要的协程,因为它们非常廉价。让我们看一下协程的几个用例。

异步操作(Asynchronous operations)

假设你正在写一个前端 UI 应用(移动端、web 端或桌面端——对于这个例子并不重要),并且需要向后端发送一个请求,以获取一些数据并使用结果更新 UI 模型。我们最初推荐这样写:

fun requestSomeData() {
    launch(UI) {
        updateUI(performRequest())
    }
}

这里,我们使用 launch(UI) 在 UI 上下文中启动一个新的协程,调用performRequest 挂起函数对后端执行异步调用,而不阻塞主 UI 线程,然后使用结果更新 UI。每个 requestSomeData 调用都创建自己的协程,这很好,不是吗?它和 C# JS 和 GO 中的异步编程并没有太大的不同。

但是这里有个问题。如果网络或后端出现问题,这些异步操作可能需要很长时间才能完成。此外,这些操作通常在一些 UI 元素(比如窗口或页面)的范围内执行。如果一个操作花费的时间太长,通常用户会关闭相应的 UI 元素并执行其他操作,或者更糟糕的是,重新打开这个 UI 并一次又一次地尝试该操作。

但是前面的操作仍然在后台运行,当用户关闭相应的 UI 元素时,我们需要某种机制来取消它。在 Kotlin 协程中,这导致我们推荐了一些非常棘手的设计模式,人们必须在代码中遵循这些模式,以确保正确处理这种取消。此外,你必须是中记住指定适当的上下文,否则 updateUI 可能会被错误的线程调用,从而破坏 UI。这是容易出错的。一个简单的launch{ … } 很容易写出来,但是你不应该写成这样。

在更哲学的层面上,很少像线程那样"全局"地启动协程。线程总是与应用程序中的某个局部作用域相关,这个局部作用域是一个生命周期有限的实体,比如 UI 元素。因此,对于结构化并发,我们现在要求在一个协程作用域中调用 launch,协程作用域是由你的生命周期有限的对象(如 UI 元素或它们相应的视图模型)实现的接口。你实现一次协程作用域后, 你会发现,在你的 UI 类中有一个简单的 launch{ … } ,然后你写很多遍,变得极容易写又正确:

fun requestSomeData() {
    launch {
        updateUI(performRequest())
    }
}

注意,协程作用域的实现还为 UI 更新定义了适当的协程上下文。对于一些比较少见的情况,你需要一个全局协程,它的生命周期受整个应用生命周期限制,我们现在提供了 GlobalScope (全局作用域)对象,因此以前全局协程的launch{ … } 变成了 GlobalScope.launch { … } ,协程的"全局"含义变得直观了。

并行分解(Parallel decomposition)

我已经就 Kotlin 协程进行了多次 [讨论],,下面的示例代码展示了如何并行加载两个图片并在稍后将它们组合起来——这是一个使用 Kotlin 协程并行分解工作的惯用示例:

suspend fun loadAndCombine(name1: String, name2: String): Image { 
    val deferred1 = async { loadImage(name1) }
    val deferred2 = async { loadImage(name2) }
    return combineImages(deferred1.await(), deferred2.await())
}

不幸的是,这个例子在很多层面上都是错误的。挂起函数loadAndCombine 本身将从一个已经启动的执行更大操作的协程内部调用。如果这个操作被取消了呢?然后加载这两个图片仍然没有收到影响。这不是我们想从可靠代码中的得到的,特别是如果这些代码是许多客户端使用后端服务的一部分。

我们推荐的解决方案是写成这样async(conroutineContext){ … } ,以便在子协程中加载两个图片,当父协程被取消时,子协程将被取消。
它仍然不完美。如果加载第一个图片失败,那么 deferred1.await() 将抛出相应的异常,但是加载第二个图片的第二个 async 协程仍然在后台工作。解决这个问题就更复杂了。

我们在第二个用例中看到了同样的问题。一个简单的 async { … } 很容易写,但是你不应该写成这样。

使用结构化并发,async 协程构建器就像 luanch 一样,变成了协程作用域上的一个扩展。你不能再简单的编写 async{ … } ,你必须提供一个作用域。并行分解的一个恰当的例子是:

suspend fun loadAndCombine(name1: String, name2: String): Image =
    coroutineScope { 
        val deferred1 = async { loadImage(name1) }
        val deferred2 = async { loadImage(name2) }
        combineImages(deferred1.await(), deferred2.await())
    }

你必须将代码封装到 coroutineScope { … } 块中,这个块为你的操作及其范围建立了边界。所有异步协程都成为这个范围的子协程,如果该作用域因为异常导致失败或被取消了,它所有的子协程也将被取消。

Kotlin 协程Kotlin Coroutines)提供了一种结构化并发的方式,可以更加方便和自然地管理异步操作和并发任务。它们可以帮助开发者避免使用传统的线程和回调函数的方式,从而提高代码的可读性和可维护性。 以下是 Kotlin 协程实现结构化并发的主要方式: 1. 使用 suspend 关键字标记异步操作的函数 使用协程时,可以将异步操作的函数声明为 suspend 函数。这些函数在执行到异步操作时可以挂起,等待异步操作完成后再继续执行。这样,代码可以更加自然地按照异步操作的顺序执行,并且可以避免回调函数的嵌套。 例如,以下是一个使用 Retrofit 库进行网络请求的示例,其中的网络请求函数使用了 suspend 关键字标记: ```kotlin suspend fun fetchUser(userId: String): User { val response = retrofitService.getUser(userId) return response.body()!! } ``` 2. 使用协程作用域来管理并发任务 协程作用域是一种用于管理协程生命周期的机制。通过使用协程作用域,可以创建一个由多个协程组成的任务,确保这些协程在同一时刻开始和结束,从而实现结构化并发。 例如,以下是一个使用协程作用域启动多个协程执行并发任务的示例: ```kotlin suspend fun fetchUserData(userIds: List<String>): List<User> = coroutineScope { userIds.map { userId -> async { fetchUser(userId) } }.awaitAll() } ``` 在这个示例中,使用了协程作用域 `coroutineScope` 来创建一个由多个协程组成的任务。每个协程都是通过 `async` 函数创建的,并在 `awaitAll` 函数中等待所有协程执行完毕后返回结果。 3. 使用协程的异常处理机制 协程还提供了一种更加自然的异常处理机制。通过使用 `try/catch` 块捕获异常,可以在异步操作出现异常时立即处理它,而不需要在回调函数中处理异常。这可以提高代码的可读性和可维护性。 例如,以下是一个使用 `try/catch` 块处理协程中的异常的示例: ```kotlin suspend fun fetchUserData(userIds: List<String>): List<User> = coroutineScope { try { userIds.map { userId -> async { fetchUser(userId) } }.awaitAll() } catch (e: Exception) { // 处理异常 emptyList() } } ``` 在这个示例中,使用了 `try/catch` 块来捕获协程中的异常,并在发生异常时返回一个空列表。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值