随着kotlinx 0.26.0版本的发布。协同程序库和Kotlin协同程序kotlinx.coroutines
结构化并发的不仅仅是一个功能,它标志着一个意识形态的巨大转变。
自2017年初Kotlin coroutines 协同程序作为Kotlin 1.1的实验特性首次推出以来,我们一直在努力向那些习惯于从线程角度考虑并发性的程序员解释协同程序的概念,因此我们的主要类比和座右铭是“协同程序是轻量级线程”。此外,我们的关键API设计为类似于线程API,以简化学习曲线。这种类比在小规模的例子中很有效,但它无助于解释协同程序编程风格的转变。
当我们被教导使用线程编程时,我们被告知线程是昂贵的资源,不应该到处创建线程。一个编写良好的程序通常在启动时创建一个线程池,然后使用它来卸载各种计算。有些环境(特别是iOS)甚至说它们“不推荐线程”(即使所有东西都仍然在线程上运行)。它们提供了一个系统范围的随时可用线程池,其中包含您可以向其提交代码的相应队列。
但coroutines协程的情况有所不同。这不仅可以,而且根据需要创建协同程序也非常方便,因为它们非常便宜。让我们看看协同程序的几个用例。
异步操作
假设您正在编写一个前端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 { … }
很容易编写,但它不是您应该编写的。
在更哲学的层面上,很少像线程那样“全局”启动协同程序。Coroutine总是与应用程序中的某些本地范围相关,后者是一个生命周期有限的实体,如UI元素。因此,对于结构化并发,我们现在要求在aCoroutineScope
中调用launch
,这是一个由生命周期有限的对象(如UI元素或其相应的视图模型)实现的接口。您只需实现一次CoroutineScope
,就可以在UI类中多次编写一个简单的启动{…},这样既容易编写又容易更正:
fun requestSomeData() {
launch {
updateUI(performRequest())
}
}
请注意,CoroutineScope
的实现还为UI更新定义了适当的协同程序上下文。
在更复杂的情况下,您的应用程序可能有许多不同的作用域,其生命周期与不同的实体相关联,因此您应该像viewModelScope
那样明确使用命名作用域。
对于那些罕见的情况,如果您需要一个全局协同程序,其生命周期受应用程序的生命周期限制,我们现在提供GlobalScope
对象,因此以前为全局协同程序launch { … }
现在变成GlobalScope
。该协同程序的全局性质在代码中变得明确。
并行分解
以下示例代码,展示了如何并行加载两个图像,并在稍后将它们组合起来——这是一个使用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
本身将从启动以执行更大操作的协同程序内部调用。如果操作被取消怎么办?然后,两幅图像的加载仍在进行中。这并不是我们想要的可靠代码,特别是如果该代码是后端服务的一部分,并且被许多客户端使用的话。
我们推荐的解决方案是编写异步(coroutineContext){…}
,以便在取消其父coroutine
时取消的子coroutine
中执行两个映像的加载。
它仍然不完美。如果第一个映像加载失败,则deferred1.await()
抛出相应的异常,但第二个异步协同程序(即加载第二个映像)仍在后台工作。解决这个问题更加复杂。
我们在第二个用例中看到了相同的问题。简单的异步很容易编写,但它不是您应该编写的。
通过结构化并发,异步协同程序构建器就像启动一样成为CoroutineScope
上的扩展。您不能再简单地编写异步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{…}
块中,该块建立操作的边界,即其范围。所有异步协同程序都成为此作用域的子进程,如果作用域因异常而失败或被取消,所有子进程也将被取消。
除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/2752.html
暂无评论