【翻译中】 proandroiddev.com/how-to-make…javascript
协程做为一种写异步代码的伟大方式,它能够完美的实现异步代码的可读性和可维护性。Kotlin提供了单一的语法结构来建立一个异步代码块:经过"suspend"关键字,和一些配套的函数库。java
在这篇文章里,我将试着用简洁的语言解释清楚协程和suspending函数的本质。为了让本文章不至于太长,我再本文不会深刻讲解协程的高级结构。重点是对协程作一个概述,而后分享下我对协程的理解。android
Kotlin团队把协程定义为"轻量级的线程"。它是一系列能够在真实的线程中执行的任务。在Kotlin官网,有这样一幅插图:express
最有意思的是,线程能够在一些特定的“暂停点”暂停执行协程,而后去作一些其余的工做。他能够在未来的某一时刻从新执行这个协程,甚至可让其余的线程来接管这个协程。因此准确的说,协程不只仅是一个“task”,而是一组有序的“子任务”按照指定的顺序依次执行。即便看起来在一个顺序的代码块中,每个对挂起函数的调用都对应启动一个协程中的新的“子任务”。promise
这就引出了咱们今天讨论的主题:挂起方法。并发
你能够找到许多相似于kotlinx的delay方法和Ktor的HttpClient.post方法。这些方法在返回前须要等待一些任务或者作一些集中的工做。这些方法都用“suspend"关键词标记。异步
suspend fun delay(timeMillis: Long) {...}
suspend fun someNetworkCallReturningValue(): SomeType {
...
}
复制代码
这类方法就被称为挂起方法。就像咱们刚才看到的:async
挂起方法能够在不阻塞当前线程的状况下暂停当前协程的执行。这意味着,在调用一个挂起方法的那一刻,当前的代码会中止执行,而且会在未来的某一时刻从新执行。然而,他并无说当前线程在这期间会作什么事儿。ide
这时它可能会返回到执行另外一个协程,而后它可能会继续执行咱们离开的协程。全部这些都由非挂起函数调用挂起函数的方式控制,可是挂起函数自己并无异步性。函数
挂起函数只有在显示的使用的时候才是异步的。咱们稍后会介绍。可是如今,您能够简单地将挂起函数视为执行过程须要一些时间的特殊函数。而且隐式将当前函数划分红几个字任务,而不用担忧线程和任务分发的复杂性。这就是为何咱们说它很棒,当你在使用它的时候,你不须要担忧这些。
您可能已经注意到,挂起函数没有特殊的返回类型。它的声明和普通函数没有区别。咱们并不须要相似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:
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协程构建器会在后台启动一个协程而且再次期间持续运行。
从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对象,我立刻就会讲到。
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的定义,主线程也将保持阻塞状态。
您可能已经注意到,不鼓励在协程内部使用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中协程概念的最重要部分。咱们能够经过使用通道、生产者和消费者等,利用协同程序来很好地表达并发的东西。但我相信,在开始构建更高抽象以前,咱们首先须要理解这些构建块。
关于协程还有不少要说的,固然这篇文章只是触及皮毛,可是我但愿这篇文章能帮助您更好地理解协程和挂起函数。
若是个人这篇文章对您有帮助的话,请告诉我,若是你想更深刻的了解某一方面的知识的话,也请告诉我。若是你发现本文的一些错误,不要犹豫,请必定指出来。