我正在阅读 Kotlin Coroutine 并且知道它基于 suspend
函数。但是 suspend
是什么意思?
协程或函数被暂停?
来自https://kotlinlang.org/docs/reference/coroutines.html
基本上,协程是可以在不阻塞线程的情况下挂起的计算
我经常听到人们说“暂停功能”。但我认为是协程因为等待函数完成而被挂起? “挂起”通常意味着“停止操作”,在这种情况下协程处于空闲状态。
我们应该说协程被暂停了吗?
哪个协程被挂起?
来自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
在 computation
上被调用,所以它可能是 async
返回 Deferred
,这意味着它可以启动另一个协程
fun computation(): Deferred<Boolean> {
return async {
true
}
}
引用说暂停协程。它是指 suspend
外部 async
协程,还是 suspend
内部 computation
协程?
suspend
是否意味着当外部 async
协程正在等待(await
)内部 computation
协程完成时,它(外部 async
协程)空闲(因此名称为挂起)并将线程返回到线程池,当子 computation
协程完成时,它(外部 async
协程)唤醒,从池中获取另一个线程并继续?
我提到线程的原因是因为 https://kotlinlang.org/docs/tutorials/coroutines-basic-jvm.html
协程等待时线程返回到池中,等待完成后,协程在池中的空闲线程上恢复
挂起函数是所有协程的核心。挂起函数只是一个可以在以后暂停和恢复的函数。他们可以执行长时间运行的操作并等待它完成而不会阻塞。
挂起函数的语法与常规函数的语法相似,只是添加了 suspend
关键字。它可以接受一个参数并有一个返回类型。但是,挂起函数只能由另一个挂起函数或在协程内调用。
suspend fun backgroundTask(param: Int): Int {
// long running operation
}
在幕后,编译器将挂起函数转换为另一个没有挂起关键字的函数,该函数采用 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)
}
但是暂停是什么意思?
标有 suspend
关键字的函数在编译时被转换为在后台异步,即使它们在源代码中看起来是同步的。
了解这种转变 IMO 的最佳来源是 Roman Elizarov 的演讲 "Deep Dive into Coroutines"。
这包括对功能的以下更改:
返回类型更改为 Unit,这是 Kotlin 表示 void 函数的方式
它获得一个额外的 Continuation
它的主体变成了一个状态机(而不是字面上使用回调,以提高效率)。这是通过将函数的主体分解成围绕所谓的悬挂点的部分,然后将这些部分变成一个大开关的分支来完成的。关于局部变量的状态以及我们在 switch 中的位置存储在 Continuation 对象中。
这是一种非常快速的描述方式,但您可以通过更多细节和演讲中的示例看到它。这整个转换基本上是“挂起/恢复”机制是如何在幕后实现的。
协程或函数被暂停?
在高层次上,我们说调用挂起函数会挂起协程,这意味着当前线程可以开始执行另一个协程。因此,协程被称为挂起而不是函数。
事实上,暂停函数的调用点因此被称为“暂停点”。
哪个协程被挂起?
让我们看看你的代码并分解发生了什么:
// 1. this call starts a new coroutine (let's call it C1).
// If there were code after it, it would be executed concurrently with
// the body of this async
async {
...
// 2. this is a regular function call, so we go to computation()'s body
val deferred = computation()
// 4. because await() is suspendING, it suspends coroutine C1.
// This means that if we had a single thread in our dispatcher,
// it would now be free to go execute C2
// 7. once C2 completes, C1 is resumed with the result `true` of C2's async
val result = deferred.await()
...
// 8. C1 can now keep going in the current thread until it gets
// suspended again (or not)
}
fun computation(): Deferred<Boolean> {
// 3. this async call starts a second coroutine (C2). Depending on the
// dispatcher you're using, you may have one or more threads.
// 3.a. If you have multiple threads, the block of this async could be
// executed in parallel of C1 in another thread
// 3.b. If you have only one thread, the block is sort of "queued" but
// not executed right away (as in an event loop)
//
// In both cases, we say that this block executes "concurrently"
// with C1, and computation() immediately returns the Deferred
// instance to its caller (unless a special dispatcher or
// coroutine start argument is used, but let's keep it simple).
return async {
// 5. this may now be executed
true
// 6. C2 is now completed, so the thread can go back to executing
// another coroutine (e.g. C1 here)
}
}
外部 async
启动协程。当它调用 computation()
时,内部 async
启动第二个协程。然后,对 await()
的调用暂停 outer async
协程的执行,直到 inner async
的协程执行结束。
您甚至可以通过单个线程看到:线程将执行外部 async
的开头,然后调用 computation()
并到达内部 async
。此时,内部 async 的主体被跳过,线程继续执行外部 async
,直到到达 await()
。 await()
是一个“暂停点”,因为 await
是一个暂停函数。这意味着外部协程被挂起,因此线程开始执行内部协程。完成后,它会返回执行外部 async
的末尾。
挂起是否意味着当外部异步协程正在等待(等待)内部计算协程完成时,它(外部异步协程)空闲(因此称为挂起)并将线程返回到线程池,并且当子计算协程完成时,它(外部异步协程)唤醒,从池中获取另一个线程并继续?
是的,确切地说。
实际上实现这一点的方法是将每个挂起函数变成一个状态机,其中每个“状态”对应于这个挂起函数内部的一个挂起点。在后台,该函数可以被多次调用,并提供有关它应该从哪个暂停点开始执行的信息(您应该真正观看我链接的视频以获取更多信息)。
async
函数但仍返回 Promise 非常烦人/令人不安。
COROUTINE_SUSPENDED
值。但是,这个特定的函数 B 永远不会实际上暂停(它永远不会返回 COROUTINE_SUSPENDED
),所以函数 A 也永远不会暂停(因为它需要接收该值),所以调度程序永远没有机会此时停止执行协程。
要了解暂停协程的确切含义,我建议您阅读以下代码:
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
}
}
}
Unconfined
协程调度器消除了 协程调度 的魔力,让我们可以直接关注裸协程。
作为 launch
调用的一部分,launch
块内的代码立即开始在当前线程上执行。发生的情况如下:
评估 val a = a() 这链接到 b(),到达suspendCoroutine。函数 b() 执行传递给 suspendCoroutine 的块,然后返回一个特殊的 COROUTINE_SUSPENDED 值。通过 Kotlin 编程模型无法观察到该值,但这就是编译后的 Java 方法所做的。函数a(),看到这个返回值,自己也返回了。启动块执行相同的操作,现在控制返回到启动调用后的行:10.downTo(0)...
请注意,此时,您的效果与 launch
块内的代码和您的 fun main
代码同时执行的效果相同。碰巧所有这些都发生在单个本机线程上,因此 launch
块被“暂停”。
现在,在 forEach
循环代码中,程序读取 b()
函数写入的 continuation
,并使用 10
的值对其进行 resumes
。 resume()
的实现方式就像 suspendCoroutine
调用返回了您传入的值一样。所以您突然发现自己正处于执行 b()
的中间。您传递给 resume()
的值被分配给 i
并对照 0
检查。如果它不为零,则 while (true)
循环在 b()
内继续,再次到达 suspendCoroutine
,此时您的 resume()
调用返回,现在您在 forEach()
中执行另一个循环步骤。这种情况一直持续到最后您使用 0
继续,然后 println
语句运行并且程序完成。
上面的分析应该给你一个重要的直觉,“暂停协程”意味着将控制权返回到最里面的 launch
调用(或者更一般地说,协程构建器)。如果协程在恢复后再次挂起,则 resume()
调用结束,控制权返回给 resume()
的调用者。
协程调度器的存在使这种推理变得不那么清晰,因为它们中的大多数会立即将您的代码提交给另一个线程。在这种情况下,上面的故事发生在另一个线程中,协程调度程序还管理 continuation
对象,因此它可以在返回值可用时恢复它。
由于已经有很多好的答案,我想为其他人发布一个更简单的例子。
运行阻塞用例:
myMethod() 是挂起函数
runBlocking { } 以阻塞方式启动协程。这类似于我们使用 Thread 类阻塞普通线程并在某些事件后通知阻塞线程的方式。
runBlocking { } 确实会阻塞当前正在执行的线程,直到协程({} 之间的主体)完成覆盖 fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.main_activity) Log.i(TAG ,"在线程上开始的外部代码:" + Thread.currentThread().name); runBlocking { Log.d(TAG,"在线程上启动的内部代码:" + Thread.currentThread().name + " 使外部代码暂停");我的方法(); } Log.i(TAG,"在线程上恢复外部代码:" + Thread.currentThread().name); } private suspend fun myMethod() { withContext(Dispatchers.Default) { for(i in 1..5) { Log.d(TAG,"Inner code i : $i on Thread :" + Thread.currentThread().name ); } }
这输出:
I/TAG: Outer code started on Thread : main
D/TAG: Inner code started on Thread : main making outer code suspend
// ---- main thread blocked here, it will wait until coroutine gets completed ----
D/TAG: Inner code i : 1 on Thread : DefaultDispatcher-worker-2
D/TAG: Inner code i : 2 on Thread : DefaultDispatcher-worker-2
D/TAG: Inner code i : 3 on Thread : DefaultDispatcher-worker-2
D/TAG: Inner code i : 4 on Thread : DefaultDispatcher-worker-2
D/TAG: Inner code i : 5 on Thread : DefaultDispatcher-worker-2
// ---- main thread resumes as coroutine is completed ----
I/TAG: Outer code resumed on Thread : main
启动用例:
launch { } 同时启动一个协程。
这意味着当我们指定启动时,协程开始在工作线程上执行。
工作线程和外线程(我们称之为启动{})都同时运行。在内部,JVM 可以执行抢占线程
当我们需要多个任务并行运行时,我们可以使用它。有一些范围指定协程的生命周期。如果我们指定 GlobalScope,协程将一直工作到应用程序生命周期结束。 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.main_activity) Log.i(TAG,"外部代码开始于线程:" + Thread.currentThread().name); GlobalScope.launch(Dispatchers.Default) { Log.d(TAG,"Inner code started on Thread : " + Thread.currentThread().name + "making external code suspend");我的方法(); } Log.i(TAG,"在线程上恢复外部代码:" + Thread.currentThread().name); } private suspend fun myMethod() { withContext(Dispatchers.Default) { for(i in 1..5) { Log.d(TAG,"Inner code i : $i on Thread :" + Thread.currentThread().name ); } } }
这输出:
10806-10806/com.example.viewmodelapp I/TAG: Outer code started on Thread : main
10806-10806/com.example.viewmodelapp I/TAG: Outer code resumed on Thread : main
// ---- In this example, main had only 2 lines to execute. So, worker thread logs start only after main thread logs complete
// ---- In some cases, where main has more work to do, the worker thread logs get overlap with main thread logs
10806-10858/com.example.viewmodelapp D/TAG: Inner code started on Thread : DefaultDispatcher-worker-1 making outer code suspend
10806-10858/com.example.viewmodelapp D/TAG: Inner code i : 1 on Thread : DefaultDispatcher-worker-1
10806-10858/com.example.viewmodelapp D/TAG: Inner code i : 2 on Thread : DefaultDispatcher-worker-1
10806-10858/com.example.viewmodelapp D/TAG: Inner code i : 3 on Thread : DefaultDispatcher-worker-1
10806-10858/com.example.viewmodelapp D/TAG: Inner code i : 4 on Thread : DefaultDispatcher-worker-1
10806-10858/com.example.viewmodelapp D/TAG: Inner code i : 5 on Thread : DefaultDispatcher-worker-1
异步和等待用例:
当我们有多个任务要做并且它们依赖于其他任务的完成时,异步和等待会有所帮助。
例如,在下面的代码中,有 2 个挂起函数 myMethod() 和 myMethod2()。 myMethod2() 应该只有在 myMethod() 完全完成后才会执行,或者 myMethod2() 取决于 myMethod() 的结果,我们可以使用 async 和 await
async 以类似于启动的方式并行启动协程。但是,它提供了一种在并行启动另一个协程之前等待一个协程的方法。
那就是等待()。异步返回 Defered
这输出:
11814-11814/? I/TAG: Outer code started on Thread : main
11814-11814/? I/TAG: Outer code resumed on Thread : main
11814-11845/? D/TAG: Inner code started on Thread : DefaultDispatcher-worker-2 making outer code suspend
11814-11845/? D/TAG: Inner code i : 1 on Thread : DefaultDispatcher-worker-2
11814-11845/? D/TAG: Inner code i : 2 on Thread : DefaultDispatcher-worker-2
11814-11845/? D/TAG: Inner code i : 3 on Thread : DefaultDispatcher-worker-2
11814-11845/? D/TAG: Inner code i : 4 on Thread : DefaultDispatcher-worker-2
11814-11845/? D/TAG: Inner code i : 5 on Thread : DefaultDispatcher-worker-2
// ---- Due to await() call, innerAsync2 will start only after innerAsync gets completed
11814-11848/? W/TAG: Inner code started on Thread : DefaultDispatcher-worker-4 making outer code suspend
11814-11848/? W/TAG: Inner code i : 1 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 2 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 3 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 4 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 5 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 6 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 7 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 8 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 9 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 10 on Thread : DefaultDispatcher-worker-4
我发现理解 suspend
的最佳方法是在 this
关键字和 coroutineContext
属性之间进行类比。
Kotlin 函数可以声明为本地或全局。本地函数神奇地可以访问 this
关键字,而全局则不能。
Kotlin 函数可以声明为 suspend
或阻塞。 suspend
函数神奇地可以访问 coroutineContext
属性,而阻塞函数则不能。
问题是:Kotlin 标准库中的 coroutineContext
属性 is declared like a "normal" property 但此声明只是用于文档/导航目的的存根。实际上 coroutineContext
是 builtin intrinsic property,这意味着在引擎盖下编译器魔术知道这个属性,就像它知道语言关键字一样。
this
关键字对本地函数的作用与 coroutineContext
属性对 suspend
函数的作用相同:它提供对当前执行上下文的访问。
因此,您需要 suspend
才能访问 coroutineContext
属性 - 当前执行的协程上下文的实例
我想给你一个关于延续概念的简单例子。这就是挂起函数的作用,它可以冻结/挂起,然后继续/恢复。不要再从线程和信号量的角度考虑协程了。从延续甚至回调挂钩的角度来考虑它。
需要明确的是,可以使用 suspend
函数暂停协程。让我们调查一下:
例如,在 android 中我们可以这样做:
var TAG = "myTAG:"
fun myMethod() { // function A in image
viewModelScope.launch(Dispatchers.Default) {
for (i in 10..15) {
if (i == 10) { //on first iteration, we will completely FREEZE this coroutine (just for loop here gets 'suspended`)
println("$TAG im a tired coroutine - let someone else print the numbers async. i'll suspend until your done")
freezePleaseIAmDoingHeavyWork()
} else
println("$TAG $i")
}
}
//this area is not suspended, you can continue doing work
}
suspend fun freezePleaseIAmDoingHeavyWork() { // function B in image
withContext(Dispatchers.Default) {
async {
//pretend this is a big network call
for (i in 1..10) {
println("$TAG $i")
delay(1_000)//delay pauses coroutine, NOT the thread. use Thread.sleep if you want to pause a thread.
}
println("$TAG phwww finished printing those numbers async now im tired, thank you for freezing, you may resume")
}
}
}
上面的代码打印以下内容:
I: myTAG: my coroutine is frozen but i can carry on to do other things
I: myTAG: im a tired coroutine - let someone else print the numbers async. i'll suspend until your done
I: myTAG: 1
I: myTAG: 2
I: myTAG: 3
I: myTAG: 4
I: myTAG: 5
I: myTAG: 6
I: myTAG: 7
I: myTAG: 8
I: myTAG: 9
I: myTAG: 10
I: myTAG: phwww finished printing those numbers async now im tired, thank you for freezing, you may resume
I: myTAG: 11
I: myTAG: 12
I: myTAG: 13
I: myTAG: 14
I: myTAG: 15
想象它像这样工作:
https://i.stack.imgur.com/371XX.png
所以你启动的当前函数不会停止,只是一个协程会在它继续的时候挂起。线程不会通过运行挂起函数来暂停。
我认为this site can help你把事情弄清楚了,是我的参考。
让我们做一些很酷的事情,并在迭代中间冻结我们的挂起函数。我们将在 onResume
稍后恢复
存储一个名为 continuation
的变量,我们将为我们加载协程延续对象:
var continuation: CancellableContinuation<String>? = null
suspend fun freezeHere() = suspendCancellableCoroutine<String> {
continuation = it
}
fun unFreeze() {
continuation?.resume("im resuming") {}
}
现在,让我们回到挂起的函数,让它在迭代过程中冻结:
suspend fun freezePleaseIAmDoingHeavyWork() {
withContext(Dispatchers.Default) {
async {
//pretend this is a big network call
for (i in 1..10) {
println("$TAG $i")
delay(1_000)
if(i == 3)
freezeHere() //dead pause, do not go any further
}
}
}
}
然后在 onResume 中的其他地方(例如):
override fun onResume() {
super.onResume()
unFreeze()
}
循环将继续。知道我们可以在任何时候冻结挂起函数并在经过一段时间后恢复它,这非常好。您还可以查看 channels
任何仍然绊倒这个问题的人,我建议快速浏览一下。我已经阅读了很多关于这个问题的误导性答案,甚至一些评价最高。这解决了我的很多疑惑。
https://youtu.be/BOHK_w09pVA?t=577
suspend
以便可以从协程中调用它。没有什么很复杂的!
这里有很多很好的答案,但我认为还有两件事需要注意。
示例中的 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 创建了一个类。它在类中为“额外”字符串创建一个属性,然后创建两个函数,一个打印“之前”,另一个打印“之后”。
编译器有效地将看起来像程序代码的内容转换为回调。
那么 suspend
关键字有什么作用呢?它告诉编译器回溯多远来寻找生成的回调需要的上下文。编译器需要知道在哪些“回调”中使用了哪些变量,suspend 关键字可以帮助它。在此示例中,在挂起之前和之后都使用了“额外”变量。因此需要将其拉出到包含编译器进行的回调的类的属性中。
它还告诉编译器这是状态的“开始”,并准备将以下代码拆分为回调。 startCoroutine
仅存在于挂起 lambda。
Kotlin 编译器生成的实际 Java 代码在这里。这是一个 switch 语句而不是回调,但实际上是同一件事。首先调用 w/case 0,然后在恢复后调用 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");
}
假设我们有一个名为 myFunction 的函数。
fun myFunction(){
Code block 1
Code block 2 //this one has a long running operation
Code block 3
Code block 4
}
通常这些代码块像 block1, block2, block3, block4 一样执行。因此,代码块 3 和 4 可能会在代码块 2 仍在运行时执行。由于这个原因,可能会出现问题。 (屏幕可能会冻结,应用程序可能会崩溃)
但是如果我们让这个函数挂起
suspend fun MyFunction(){
Code block 1
Code block 2 //this one has a long running operation
Code block 3
Code block 4
}
现在,这个函数可以在代码块 2(长时间运行的操作)开始执行时暂停,并在完成后恢复。代码块 3 和 4 将在此之后执行。所以不会有意外的线程共享问题。
对于仍然想知道我们如何实际挂起挂起函数的人,我们在挂起函数的主体中使用了 suspendCoroutine 函数。
suspend fun foo() :Int
{
Log.d(TAG,"Starting suspension")
return suspendCoroutine<Int> { num->
val result = bar()
Log.d(TAG,"Starting resumption")
num.resumeWith(Result.success(result))
}
}
fun bar():Int //this is a long runnning task
suspendCoroutine
主要与一些带有回调的旧代码一起使用。在这里使用 suspendCoroutine
有什么意义?它不会将上下文切换到后台线程,因此它会阻塞协程运行的线程。如果协程使用 Dispatchers.Main
上下文,它将阻塞主线程。
suspend fun
可以暂停,但究竟如何?long running operation
中放入任何内容,并且线程只会在它认为合适的任何地方暂停执行。这听起来不准确。从挂起函数内部调用阻塞函数仍然会阻塞它正在运行的线程。挂起函数将运行,如果它发现另一个挂起函数调用,它将保存其状态,并释放线程以供调用堆栈中的下一个任务运行。当该任务完成时,原始功能将恢复。无论如何,这就是我的理解:youtu.be/BOHK_w09pVA?t=577