我正在阅读Kotlin协程,并且知道它是基于挂起函数的。但是暂停是什么意思呢?
协程或函数被挂起?
从https://kotlinlang.org/docs/reference/coroutines.html
基本上,协程是可以挂起而不阻塞线程的计算
我经常听到人们说“暂停功能”。但我认为是协程被挂起,因为它正在等待函数完成?“suspend”通常意味着“停止操作”,在这种情况下,协程是空闲的。
我们是否应该说协程被挂起?
哪个协程被挂起?
从https://kotlinlang.org/docs/reference/coroutines.html
继续类比,await()可以是一个挂起函数(因此也可以从async{}块中调用),它挂起协程,直到完成一些计算并返回其结果:
async { // Here I call it the outer async coroutine
...
// Here I call computation the inner coroutine
val result = computation.await()
...
}
它说“这会挂起一个协程,直到一些计算完成”,但协程就像一个轻量级线程。那么,如果协程挂起,如何进行计算呢?
我们看到await在计算时被调用,所以返回Deferred的可能是异步的,这意味着它可以启动另一个协程
fun computation(): Deferred<Boolean> {
return async {
true
}
}
这句话说挂起了一个协程。这意味着挂起外部异步协程,还是挂起内部计算协程?
挂起是否意味着当外部异步协程正在等待(await)内部计算协程完成时,它(外部异步协程)空闲(因此称为挂起)并返回线程池,当子计算协程完成时,它(外部异步协程)醒来,从池中取出另一个线程并继续?
我之所以提到这个线程是因为https://kotlinlang.org/docs/tutorials/coroutines-basic-jvm.html
当协程正在等待时,线程被返回到池中,当等待完成时,协程在池中的空闲线程上恢复
这里有很多很好的答案,但我认为还有两件重要的事情需要注意。
例子中的launch / withContext / runBlocking和其他很多东西都来自协程库。其实和暂停没有任何关系。使用协程不需要协程库。协程是编译器的一个“技巧”。是的,库确实让事情变得更简单,但编译器正在做挂起和恢复事情的魔法。
第二件事,编译器只是把看起来是过程的代码转换成内部的回调。
取以下最小协程,挂起不使用协程库的协程:
lateinit var context: Continuation<Unit>
suspend {
val extra="extra"
println("before suspend $extra")
suspendCoroutine<Unit> { context = it }
println("after suspend $extra")
}.startCoroutine(
object : Continuation<Unit> {
override val context: CoroutineContext = EmptyCoroutineContext
// called when a coroutine ends. do nothing.
override fun resumeWith(result: Result<Unit>) {
result.onFailure { ex : Throwable -> throw ex }
}
}
)
println("kick it")
context.resume(Unit)
我认为理解它的一个重要方法是看看编译器对这些代码做了什么。实际上,它为lambda创建了一个类。它在类中为“extra”字符串创建了一个属性,然后创建了两个函数,一个打印“之前”,另一个打印“之后”。
实际上,编译器将看起来像过程代码的代码转换为回调。
那么,suspend关键字是做什么的呢?它告诉编译器要回溯到多远的地方去寻找生成的回调所需要的上下文。编译器需要知道在哪个“回调”中使用了哪些变量,而suspend关键字可以帮助它。在这个例子中,“extra”变量在挂起之前和之后都被使用。因此,它需要被拉出到包含编译器所做回调的类的属性中。
它还告诉编译器这是状态的“开始”,并准备将下面的代码分解为回调。startCoroutine只存在于挂起lambda上。
Kotlin编译器生成的实际Java代码在这里。这是一个switch语句而不是回调,但实际上是一样的。首先调用w/ case 0,然后在恢复后调用w/ case 1。
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
var10_2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch (this.label) {
case 0: {
ResultKt.throwOnFailure((Object)$result);
extra = "extra";
var3_4 = "before delay " + extra;
var4_9 = false;
System.out.println((Object)var3_4);
var3_5 = this;
var4_9 = false;
var5_10 = false;
this.L$0 = extra;
this.L$1 = var3_5;
this.label = 1;
var5_11 = var3_5;
var6_12 = false;
var7_13 = new SafeContinuation(IntrinsicsKt.intercepted((Continuation)var5_11));
it = (Continuation)var7_13;
$i$a$-suspendCoroutine-AppKt$main$1$1 = false;
this.$context.element = it;
v0 = var7_13.getOrThrow();
if (v0 == IntrinsicsKt.getCOROUTINE_SUSPENDED()) {
DebugProbesKt.probeCoroutineSuspended((Continuation)var3_5);
}
v1 = v0;
if (v0 == var10_2) {
return var10_2;
}
** GOTO lbl33
}
case 1: {
var3_6 = this.L$1;
extra = (String)this.L$0;
ResultKt.throwOnFailure((Object)$result);
v1 = $result;
lbl33:
// 2 sources
var3_8 = "after suspend " + extra;
var4_9 = false;
System.out.println((Object)var3_8);
return Unit.INSTANCE;
}
}
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
为了理解挂起协程到底意味着什么,我建议你看一下下面的代码:
import kotlinx.coroutines.Dispatchers.Unconfined
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
var continuation: Continuation<Int>? = null
fun main() {
GlobalScope.launch(Unconfined) {
val a = a()
println("Result is $a")
}
10.downTo(0).forEach {
continuation!!.resume(it)
}
}
suspend fun a(): Int {
return b()
}
suspend fun b(): Int {
while (true) {
val i = suspendCoroutine<Int> { cont -> continuation = cont }
if (i == 0) {
return 0
}
}
}
unrestricted协程调度程序消除了协程调度的魔力,允许我们直接关注裸协程。
作为启动调用的一部分,启动块内的代码立即在当前线程上开始执行。具体情况如下:
计算val a = a()
这将链到b(),到达suspendCoroutine。
函数b()执行传递给suspendCoroutine的块,然后返回一个特殊的COROUTINE_SUSPENDED值。通过Kotlin编程模型无法观察到这个值,但这正是编译后的Java方法所做的。
函数a()看到这个返回值,本身也返回它。
启动块做同样的事情,现在控制返回到启动调用之后的行:
注意,在这一点上,您可以获得与启动块内的代码和有趣的主代码同时执行相同的效果。所有这些都发生在单个本机线程上,因此启动块被“挂起”。
Now, inside the forEach looping code, the program reads the continuation that the b() function wrote and resumes it with the value of 10. resume() is implemented in such a way that it will be as if the suspendCoroutine call returned with the value you passed in. So you suddenly find yourself in the middle of executing b(). The value you passed to resume() gets assigned to i and checked against 0. If it's not zero, the while (true) loop goes on inside b(), again reaching suspendCoroutine, at which point your resume() call returns, and now you go through another looping step in forEach(). This goes on until finally you resume with 0, then the println statement runs and the program completes.
上面的分析应该给您一个重要的直观印象,即“挂起一个协程”意味着将控件返回到最内部的启动调用(或者更一般地说,是协程构建器)。如果协程在恢复后再次挂起,则resume()调用结束,控制权返回给resume()的调用者。
协程分派器的存在使得这种推理不那么清晰,因为它们中的大多数会立即将您的代码提交给另一个线程。在这种情况下,上面的故事发生在另一个线程中,协程调度程序还管理延续对象,以便在返回值可用时恢复它。
挂起函数是所有协程的中心。
挂起函数只是一个可以在以后暂停和恢复的函数。它们可以执行一个长时间运行的操作,并等待它完成而不阻塞。
暂停函数的语法与常规函数相似,只是增加了suspend关键字。它可以接受一个参数并具有返回类型。但是,挂起函数只能由另一个挂起函数或在协程中调用。
suspend fun backgroundTask(param: Int): Int {
// long running operation
}
在底层,挂起函数由编译器转换为另一个不带suspend关键字的函数,该函数接受一个类型为Continuation<T>的附加形参。例如,上面的函数将被编译器转换为:
fun backgroundTask(param: Int, callback: Continuation<Int>): Int {
// long running operation
}
Continuation<T>是一个包含两个函数的接口,如果函数挂起时发生错误,则调用这两个函数以返回值或异常恢复协程。
interface Continuation<in T> {
val context: CoroutineContext
fun resume(value: T)
fun resumeWithException(exception: Throwable)
}