【翻译中】 proandroiddev.com/how-to-make…
协程作为一种写异步代码的伟大方式,它可以完美的实现异步代码的可读性和可维护性。Kotlin提供了单一的语法结构来创建一个异步代码块:通过"suspend"关键字,和一些配套的函数库。
在这篇文章里,我将试着用简洁的语言解释清楚协程和suspending函数的本质。为了让本文章不至于太长,我再本文不会深入讲解协程的高级结构。重点是对协程做一个概述,然后分享下我对协程的理解。
什么是协程?
Kotlin团队把协程定义为"轻量级的线程"。它是一系列可以在真实的线程中执行的任务。在Kotlin官网,有这样一幅插图:
最有意思的是,线程可以在一些特定的“暂停点”暂停执行协程,然后去做一些其他的工作。他可以在将来的某一时刻重新执行这个协程,甚至可以让其他的线程来接管这个协程。所以准确的说,协程不仅仅是一个“task”,而是一组有序的“子任务”按照指定的顺序依次执行。即使看起来在一个顺序的代码块中,每一个对挂起函数的调用都对应启动一个协程中的新的“子任务”。
这就引出了我们今天讨论的主题:挂起方法。
挂起方法
你可以找到许多类似于kotlinx的delay方法和Ktor的HttpClient.post方法。这些方法在返回前需要等待一些任务或者做一些集中的工作。这些方法都用“suspend"关键词标记。
suspend fun delay(timeMillis: Long) {...}
suspend fun someNetworkCallReturningValue(): SomeType {
...
}
复制代码
这类方法就被称为挂起方法。就像我们刚才看到的:
挂起方法可以在不阻塞当前线程的情况下暂停当前协程的执行。这意味着,在调用一个挂起方法的那一刻,当前的代码会停止执行,并且会在将来的某一时刻重新执行。然而,他并没有说当前线程在这期间会做什么事儿。
这时它可能会返回到执行另一个协程,然后它可能会继续执行我们离开的协程。所有这些都由非挂起函数调用挂起函数的方式控制,但是挂起函数本身并没有异步性。
挂起函数只有在显示的使用的时候才是异步的。我们稍后会介绍。但是现在,您可以简单地将挂起函数视为执行过程需要一些时间的特殊函数。并且隐式将当前函数划分成几个字任务,而不用担心线程和任务分发的复杂性。这就是为什么我们说它很棒,当你在使用它的时候,你不需要担心这些。
挂起的世界是有序的
您可能已经注意到,挂起函数没有特殊的返回类型。它的声明和普通函数没有区别。我们并不需要类似Java的Future或者JavaScript的Promise这样的包装类。这进一步证明了挂起函数本身不是异步的,不像JavaScript的异步函数,返回的是promises。
从挂起函数内部,我们可以对函数的调用顺序进行推理。
这就是为什么在Kotlin中异步的东西很容易推理。在挂起函数内部,对其他挂起函数的调用与普通函数调用的行为类似:在获取返回值并执行其余代码之前,我们需要等待被调用函数的执行。
suspend fun someNetworkCallReturningSomething(): Something {
// some networking operations making use of the suspending mechanism
}
suspend fun someBusyFunction(): Unit {
delay(1000L)
println("Printed after 1 second")
val something: Something = someNetworkCallReturningSomething()
println("Received $something from network")
}
复制代码
这将允许我们稍后以简单的方式编写复杂的异步代码。
连接普通世界和挂载世界
在“普通”函数中直接调用挂起函数是不被允许的。通常的解释是“因为只有协程可以被挂起”,从这里我们得出结论,我们需要创建一个协程来运行我们的挂起函数。这很棒。但是为什么呢?
从概念上讲,挂起函数在某种程度上从它们的声明中宣布它们可能“需要一些时间来执行”。如果您自己不是一个挂起函数,这将强制您显式地执行以下两种操作之一:
在等待时阻塞线程(就像普通的同步函数调用一样)
使用异步方式为您完成任务,并立即返回(有多种方式来实现)
通过创建协程来实现可以作为您的一种选择,这种选择必须是显式的(这很棒!)这是通过使用称为协程构建器的函数来实现的。
协程构建器
协程构建器是一个简单的方法,用来创建一个新的协程,来运行一个挂载方法。它可以在一个普通的方法里调用,由于他们自己没有被挂起,因此他们可以充当正常与挂起世界之间的桥梁。
Kotlin标准库包含了多种协程构造器来构造一系列的协程。我们会在下面的章节里介绍其中的几种。
通过“runBlocking”来阻塞当前的线程。
在一个普通方法里处理一个挂起方法的最简单的方式是阻塞当前的线程,然后等待。阻塞当前线程的协程构造器叫做 runBlocking:
fun main() {
println("Hello,")
// we create a coroutine running the provided suspending lambda
// and block the main thread while waiting for the coroutine to finish its execution
runBlocking {
// now we are inside a coroutine
delay(2000L) // suspends the current coroutine for 2 seconds
}
// will be executed after 2 seconds
println("World!")
}
复制代码
在runBlocking的环境下,给定的挂起方法以及他的调用层级会一直有效的阻塞当前的线程,直到它执行完成。
从这个方法的签名中可以看出来,传递给runBlocking的方法是一个挂起方法,即使runBlocking本身不是可挂载的(它是线程阻塞的)
fun <T> runBlocking(
...,
block: suspend CoroutineScope.() -> T
): T {
...
}
复制代码
"runBlocking"经常被用在main()函数里,用来创建一些顶级协程,并且保持JVM的存活(我们将在关于结构化并发的那部分介绍中看到这一点)。
通过“launch”,发射然后遗忘
通常情况下,协程的目的不是为了阻塞线程,而是为了启动一个异步任务。launch协程构建器会在后台启动一个协程并且再次期间持续运行。
从Kotlin的官方文档中,我们可以看到下面这个例子:
fun main() {
GlobalScope.launch { // launch new coroutine in background and continue
delay(1000L)
println("World!")
}
println("Hello,") // main thread continues here immediately
runBlocking { // but this expression blocks the main thread
delay(2000L) // ... while we delay for 2 seconds to keep JVM alive
}
}
复制代码
通过注释我们可以知道,这个例子首先会立刻在terminal打印出“Hello,”,过一秒后会打印出“World!”。
注意,为了达到我们举这个例子的目的,看到启动后究竟发生了什么,我们需要以某种方式来阻塞main函数。这是为什我们一直用重复使用re-using,是为了保持JVM的存活。(我们也可以用Thread.sleep()来实现,但那样的话就太不Kotlin了,不是吗?)
不用担心这个GlobalScope对象,我马上就会讲到。
通过“async”异步获取结果
Here is another coroutine builder called async which allows to perform an asynchronous operation returning a value:
这是另一个名为async的协程构建器,它允许执行有返回值的异步操作:
为了得到延迟值的结果,async返回一个方便的延迟对象,它类似于Future或Promise。我们可以对这个延迟值调用wait,以便等待它执行完并获得结果。
wait不是一个普通的阻塞函数,它是一个挂起函数。这意味着我们不能直接从main()函数中调用它。为了等待结果,我们需要以某种方式阻塞main函数,因此我们在这里使用runBlocking来封装这个await调用。
眼睛犀利如你或许已经注意到了,GlobalScope再次出现在这里了,因为我在这里聊聊它了。
结构化的并发性
如果您已经理解了上面的几个例子,您可能已经注意到我们需要熟悉经典的“block and wait for my coroutines to finish”模式。
在Java中,这通常是通过保持对线程的引用并对所有线程调用join来获得的,以便在等待所有其他线程时阻塞主线程。我们可以用Kotlin协程做类似的事情,但这不是Kotlin的习惯用法。
在Kotlin中,可以在层次结构中创建协作程序,这允许父协作程序为您自动管理其子协作程序的生命周期。例如,它可以等待其子节点完成,或者在其中一个异常中发生时取消所有子节点。
创建协程的层次结构
除了不应该从协程调用runblock之外,所有协程构建器都声明为CoroutineScope类的扩展,以鼓励人们构造协程:
fun <T> runBlocking(...): T {...}fun <T> CoroutineScope.async(...): Deferred<T> {...}
fun <T> CoroutineScope.launch(...): Job {...}
fun <E> CoroutineScope.produce(...): ReceiveChannel<E> {...}
...
复制代码
为了创建一个协同程序,您要么需要在GlobalScope上调用这些构建器(创建顶级协同程序),要么需要从一个已经存在的协同程序范围(创建该范围的子协同程序)调用这些构建器。事实上,如果您编写一个创建协程的函数,您也应该将它声明为CoroutineScope类的扩展。这是一种约定,允许您轻松调用coroutine构建器,因为您可以这样使用CoroutineScope。
If you take a look at coroutine builders’ signatures, you may notice that the suspending function they take as a parameter is also defined as an extension function of the CoroutineScope class:
如果你仔细看下协程构建器的签名,你会发现,被当做参数的挂载方法也是CoroutineScope类的一个扩展:
fun <T> CoroutineScope.async(
...
block: suspend CoroutineScope.() -> T
): Deferred<T> {
...
}
复制代码
这意味着我们可以在该函数中调用其他协程构建器而不指定任何接收器,隐式接收器将是当前协程的子范围,使其充当父进程。这很Easy吧!
下面是我们应该如何用更习惯的方式来组织前面的例子:
fun main() = runBlocking {
val deferredResult = async {
delay(1000L)
"World!"
}
println("Hello, ${deferredResult.await()}")
}
复制代码
fun main() = runBlocking {
launch {
delay(1000L)
println("World!")
}
println("Hello,")
}
复制代码
fun main() = runBlocking {
delay(1000L)
println("Hello, World!")
}
复制代码
注意,我们不再需要GlobalScope,因为范围是由包装runBlocking调用提供的。我们也不需要额外的延迟来等待子协同程序完成。runblock将等待它的所有子线程完成,然后再完成它自己的执行,因此根据runblock的定义,主线程也将保持阻塞状态。
coroutineScope构建器
您可能已经注意到,不鼓励在协程内部使用runBlocking。这是因为Kotlin团队希望避免协同程序中的线程阻塞函数,而是使用挂起操作。与runBlocking等价的挂起机制是coroutineScope构建器。
coroutineScope只是挂起当前的协同程序,直到所有子协同程序都执行完毕。下面是直接取自Kotlin文档的例子:
fun main() = runBlocking { // this: CoroutineScope
launch {
delay(200L)
println("Task from runBlocking")
}
coroutineScope { // Creates a new coroutine scope
launch {
delay(500L)
println("Task from nested launch")
}
delay(100L)
println("Task from coroutine scope") // This line will be printed before nested launch
}
println("Coroutine scope is over") // This line is not printed until nested launch completes
}
复制代码
我在这里讲解的基本构建块实际上并不是Kotlin中协程概念的最重要部分。我们可以通过使用通道、生产者和消费者等,利用协同程序来很好地表达并发的东西。但我相信,在开始构建更高抽象之前,我们首先需要理解这些构建块。
关于协程还有很多要说的,当然这篇文章只是触及皮毛,但是我希望这篇文章能帮助您更好地理解协程和挂起函数。
如果我的这篇文章对您有帮助的话,请告诉我,如果你想更深入的了解某一方面的知识的话,也请告诉我。如果你发现本文的一些错误,不要犹豫,请一定指出来。