有可能,在你的程序员生涯中,你已经处理过一些常见的问题,如线程饥饿、回调地狱、线程被阻塞的时间超过了它们应该有的时间。事实上,与线程打交道并不那么容易,尤其是当你的系统以异步风格的例程为目标时。
许多语言已经为异步编码开发了简化功能--比如Go的Goroutines,它基本上是由Go运行时管理的轻量级线程。Closure提供了类似的功能,它的core.async设施用于异步编程,Node.js提供了臭名昭著的事件循环,现在Kotlin也提供了coroutines。
在这篇文章中,我们将探索Kotlin coroutines的新兴领域,试图展示它们如何简化语言中的异步编程。
是什么让Kotlin coroutines变得独一无二?
Kotlin没有其他语言所具有的默认异步功能,比如JavaScript中内置的保留词async 和await 。相反,JetBrains在kotlinx-coroutines库下发布了一套新的coroutines,其中有几个高级coroutines用于各种任务,如launch 和async ,等等。
看看下面这个从JetBrains提供的操场环境中提取的例子吧。
suspend fun main() = coroutineScope {
launch {
delay(1000)
println("Kotlin Coroutines World!")
}
println("Hello")
}
你认为哪个打印行会先被打印出来?如果你的答案是 "Hello",你是对的。发生这种情况是因为我们把launch 块延迟了一秒钟,而第二个打印则没有。
就其核心而言,一个coroutine只不过是一个简单的、轻量级的线程。就像我们过去用Java一样,它们需要显式启动,你可以通过launch coroutine builder在coroutineScope (例如,在全局范围内,只要应用程序活着,coroutine就会一直存在)的上下文下进行。
coroutineScope 构建器创建了一个冠词作用域,它在执行自己的完成之前会等待所有的子冠词完成。
对于那些希望将不同的程序归入一个更全局的程序之下的人来说,这是一个伟大的功能。而且它的概念与 [runBlocking](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html),它阻断了当前线程的等待,以对抗coroutineScope 带来的刚刚暂停的模式。
在我们上面的例子中,delay 函数使用了Thread 作用域,可以用这样的函数来代替。
launch {
Thread.sleep(1000)
println("Kotlin Coroutines World!")
}
而launch 函数,可以用等价的函数Thread 来代替。
在例子中改变它时要小心,因为delay 函数,也是一个suspend 函数,只能从一个冠词或另一个suspend 函数调用。
基于这些术语,我们的代码例子将迁移到下面。
import kotlinx.coroutines.*
import kotlin.concurrent.thread
suspend fun main() = coroutineScope {
thread {
Thread.sleep(1000)
println("Kotlin Coroutines World!")
}
println("Hello")
}
COROUTINE的一个巨大优势是,它们可以在运行的线程内暂停执行,次数不限。这意味着我们在资源方面节省了很多,因为无限停止的线程等待执行完成已经不是经验法则了。
如果你想等待一个特定的程序完成,你也可以这样做。
val job = GlobalScope.launch {
delay(1000L)
println("Coroutines!")
}
println("Hello,")
job.join()
我们在这里创建的引用被称为后台作业,它是一个可取消的任务,其生命周期以其完成为高潮。join 函数会等待,直到coroutine完成。
这是一个非常有用的概念,在你想对一些coroutine的完成的同步状态有更多控制的情况下,可以采用这个概念。但Kotlin是如何实现的呢?
延续传递式
CPS,或称延续传递式,是一种编程类型,其工作原理是允许控制流以延续的形式明确传递--即作为计算机程序流的控制状态的抽象表示。它非常类似于JavaScript中著名的回调函数。
为了更好地理解它,让我们看一下 [Continuation](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/-continuation/)接口。
interface Continuation<in T> {
val context: CoroutineContext
fun resume(value: T)
fun resumeWith(result: Result<T>)
fun resumeWithException(exception: Throwable)
}
那代表了一个暂停点之后的延续,返回一个类型为T 的值。在它的主要对象和函数中,我们有。
context:与该延续的上下文链接resumeXXX: 用不同的结果恢复相应的coroutine的执行的函数
很好!现在,让我们来看看一个更实际的例子。想象一下,你正在处理一个普通的函数,该函数通过一个暂停的函数从你的数据库中检索信息。
suspend fun slowQueryById(id: Int): Data {
delay(1000)
return Data(id = id, ... )
}
比方说,那里的delay 函数模拟了你为获得数据结果而必须运行的缓慢查询。
在幕后,Kotlin通过另一个被称为状态机的概念将coroutine转换为一种回调函数,而不是创建大量的新函数。
取消与超时
我们已经学习了如何创建后台作业以及如何等待它们完成。我们还看到,这些作业是可取消的结构,这意味着,如果你对它们的结果不再感兴趣,你可能想取消它们,而不是等待它们完成。
在这种情况下,只需调用cancel 函数。
job.cancel()
然而,有时你也想在取消某些操作或等待它们完成之前为它们建立一个限制。这时,超时就变得很方便。
如果一个给定的操作花费的时间超过了它应该花费的时间,那么timeout 配置将确保抛出一个适当的异常,以便你做出相应的反应。
runBlocking {
withTimeout(2000L) {
repeat(100) {
delay(500L)
}
}
}
如果操作超过了我们设定的两秒的时间限制,就会抛出一个CancellationException 错误。
另一个版本是通过withTimeoutOrNull 块来实现的。让我们看一个例子。
import kotlinx.coroutines.*
suspend fun main() = runBlocking<Unit> {
withTimeoutOrNull(350) {
for (i in 1..5) {
delay(100)
println("Current number: $i")
}
}
}
在这里,只有数字1到3会被打印出来,因为超时被设置为350ms。我们每次迭代都有100ms的延迟,这只够填充我们的for 的三个值。
这对于你不希望抛出异常的场景也很好。
采用异步方式
如果你以前使用过JavaScript,你可能习惯于创建async 函数,并确保在同步块中期待结果时await 。
Kotlin允许我们通过async coroutine来做同样的事情。假设你想启动两个不同的硬处理线程,并等待两个结果返回到主线程。下面是一个例子,暴露了Kotlin是如何利用Java的特性的,例如 [Future](https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Future.html):
val thread1 = async(CommonPool) {
// hard processing 1
}
val thread2 = async(CommonPool) {
// hard processing 2
}
runBlocking {
thread1.await()
thread2.await()
}
async 函数创建一个新的coroutine,并将其未来的结果作为Deferred 的实现返回。当产生的Deferred 被取消时,运行中的coroutine也被取消。
Deferred ,反过来,是一个非阻塞的可取消的未来--也就是说,它是一个有结果的Job 。
当两个硬处理程序启动时,主程序通过runBlocking 执行调用被暂停,只有在两个线程的结果可用后才会恢复。这样一来,我们就获得了性能,因为两个程序将被并行执行。
为异步数据流构建流程
Kotlin还为我们提供了一个处理异步数据流的好方法。有时你需要你的流来发射数值,通过一些外部的异步函数来转换它们,收集结果,并成功地完成流或出现异常。
如果是这样的话,我们可以利用 [Flow<T>](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html)类型。让我们来看看下面这个例子,它在一个数字序列上进行迭代,并打印出它的每个值。
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
suspend fun main() = runBlocking<Unit> {
(1..3).asFlow().collect { value -> println("Current number: $value") }
}
如果你习惯于使用Java Streams API,或者其他语言的类似版本,这些代码可能对你来说非常熟悉。
Kotlin也提供了辅助函数,map 和filter 操作,尽管它们内部可能有长时间的异步调用。
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
suspend fun main() = runBlocking<Unit> {
(1..5).asFlow()
.filter{ number -> number % 2 == 0 } // only even numbers
.map{ number -> convertToStr(number) } // converts to string
.collect { value -> println(value) }
}
suspend fun convertToStr(request: Int): String {
delay(1000)
return "Current number: $request"
}
结论
很高兴看到Kotlin朝着创造一个更加异步和非阻塞的世界迈出了一步。虽然Kotlin的coroutines相对较新,但它们已经利用了其他语言长期以来从这种范式中提取的巨大潜力。
The postUnderstanding Kotlin coroutinesappeared first onLogRocket Blog.