我正在阅读KotlinCoroutine,知道它是基于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
协程)空闲(因此命名为suspend)并将thread返回到线程池,当子computation
协程完成时,它将返回线程池。(外部async
协程)唤醒,从池中取出另一个线程并继续?
我提到这个线程的原因是因为https://kotlinlang.org/docs/tutorials/coroutines-basic-jvm.html
线程在协程等待时返回到池中,等待完成后,协程在池中的一个空闲线程上恢复
9条答案
按热度按时间vsdwdz231#
suspend是什么意思
用
suspend
关键字标记的函数在编译时被转换为异步(在字节码中),即使它们在源代码中显示为同步。了解这种转变IMO的最佳来源是Roman Elizarov的谈话"Deep Dive into Coroutines"。
例如,此函数:
转换为(为了简单起见,用Java而不是实际的JVM字节码表示):
这包括对函数的以下更改:
Object
(相当于Kotlin的Any?
-包含所有值的类型),以允许返回一个特殊的COROUTINE_SUSPENDED
令牌来表示协程何时实际挂起Continuation<X>
参数(其中X是代码中声明的函数的前一个返回类型-在示例中它是String
)。Continuation
对象中。这是一种非常快速的描述方式,但您可以在演讲中看到更多细节和示例。整个转换基本上是如何实现“挂起/恢复”机制的。
协程或函数被挂起?
在高层,我们说调用suspending函数suspendsthe coroutine,意思是当前线程可以开始执行另一个协程,所以,被挂起的是 coroutine 而不是函数。
实际上,挂起函数的调用位置因此被称为“挂起点”。
哪个协程被挂起?
让我们看看你的代码,并分解发生了什么(编号遵循执行时间轴):
外部
async
启动一个协程。当它调用computation()
时,内部async
启动第二个协程。然后,对await()
的调用挂起 * 外部 *async
协程的执行,直到 * 内部 *async
的协程执行结束。你甚至可以用一个线程看到:线程将执行外部
async
的开始,然后调用computation()
并到达内部async
。此时,跳过内部async的主体,并且线程继续执行外部async
直到其到达await()
。await()
是“挂起点”,因为await
是一个挂起函数。这意味着外部协程被挂起,因此线程开始执行内部协程。当它完成时,它会返回执行外部async
的末尾。suspend是否意味着当外部异步协程等待(await)内部计算协程完成时,它(外部异步协程)空闲(因此得名suspend)并将线程返回到线程池,当子计算协程完成时,它(外部异步协程)唤醒,从池中取出另一个线程并继续?
是的,没错。
实际上实现的方法是将每个挂起函数转换为一个状态机,其中每个“状态”对应于该挂起函数中的一个挂起点。在后台,该函数可以被多次调用,并提供有关它应该从哪个挂起点开始执行的信息(您应该观看我链接的视频以了解更多信息)。
relj7zay2#
要理解挂起协程的确切含义,我建议您查看以下代码:
Unconfined
协程调度器消除了 * 协程调度 * 的魔力,并允许我们直接关注裸协程。作为
launch
调用的一部分,launch
块中的代码立即开始在当前线程上执行。发生的情况如下:1.评估
val a = a()
1.这链接到
b()
,达到suspendCoroutine
。1.函数
b()
执行传递给suspendCoroutine
的块,然后返回一个特殊的COROUTINE_SUSPENDED
值。这个值在Kotlin编程模型中是不可观察的,但编译后的Java方法就是这样做的。1.函数
a()
看到这个返回值,它本身也返回它。launch
块也是如此,现在控制返回到launch
调用之后的行:10.downTo(0)...
请注意,此时,
launch
块中的代码和fun main
代码并发执行的效果相同。只是所有这些都发生在单个本机线程上,因此launch
块被“挂起”。现在,在
forEach
循环代码中,程序读取b()
函数写入的continuation
,并将resumes
的值写入10
。resume()
的实现方式是,就好像suspendCoroutine
调用返回的是您传入的值。因此,您突然发现自己正在执行b()
。您传递给resume()
的值被分配给i
,并根据0
进行检查。如果它不为零,则while (true)
循环将在b()
内部继续。再次到达suspendCoroutine
,此时您的resume()
调用返回,现在您在forEach()
中执行另一个循环步骤。这一直持续到最后您使用0
继续执行,然后println
语句运行,程序完成。上面的分析应该给予你一个重要的直觉,即“挂起一个协程”意味着将控制返回到最里面的
launch
调用(或者更一般地说,* 协程构建器 *)。如果协程在恢复后再次挂起,则resume()
调用结束,控制返回到resume()
的调用者。协程调度器的存在使得这个推理不那么明确,因为大多数协程调度器会立即将您的代码提交给另一个线程。在这种情况下,上述故事发生在另一个线程中,协程调度器也管理
continuation
对象,以便在返回值可用时恢复它。zvokhttg3#
因为已经有很多好的答案了,我想为其他人发布一个更简单的例子。
runBlocking用例:
suspend
函数runBlocking { }
以阻塞的方式启动一个协程。这类似于我们如何使用Thread
类阻塞正常线程,并在某些事件发生后通知阻塞的线程。runBlocking { }
会阻塞当前正在执行的线程,直到协程({}
之间的主体)完成这将输出:
启动用例:
launch { }
同时启动一个协程。worker
线程上开始执行。worker
线程和外部线程(我们称之为launch { }
)都是并发运行的。JVM内部可能会执行抢占式线程scopes
指定了协程的生存期。如果我们指定GlobalScope
,协程将一直工作到应用程序的生存期结束。此输出:
async和await用例:
async
和await
会有所帮助。2
挂起函数myMethod()和myMethod2()。myMethod2()
应该只有在myMethod()
完全完成后才执行或myMethod2()
取决于myMethod()
的结果,我们可以使用async
和await
async
以类似于launch
的方式并行启动协程。但是,它提供了一种在并行启动另一个协程之前等待一个协程的方法。await()
.async
返回Deffered<T>
的示例.T
默认为Unit
.当我们需要等待任何async
完成时,我们需要在async
的Deffered<T>
示例上调用.await()
.如下例所示,我们调用innerAsync.await()
,这意味着执行将被挂起,直到innerAsync
完成。我们可以在输出中观察到相同的情况。innerAsync
首先完成,这调用myMethod()
。然后async
innerAsync2
启动,这调用myMethod2()
这将输出:
093gszye4#
我发现理解
suspend
的最好方法是在this
关键字和coroutineContext
属性之间进行类比。Kotlin函数可以声明为local或global。Local函数神奇地访问了
this
关键字,而global函数却没有。Kotlin函数可以声明为
suspend
或blocking。suspend
函数神奇地访问coroutineContext
属性,而blocking函数则不能。问题是:
coroutineContext
属性在Kotlinstdlib中被声明为一个“普通”属性,但这个声明只是一个用于文档/导航目的的存根。实际上,coroutineContext
是内置的固有属性,这意味着编译器可以像知道语言关键字一样知道这个属性。this
关键字对本地函数的作用与coroutineContext
属性对suspend
函数的作用相同:它提供对当前执行上下文的访问。因此,您需要
suspend
来访问coroutineContext
属性-当前执行的协程上下文的示例46qrfjad5#
我想给予你一个关于continuation概念的简单例子。这就是suspend函数的作用,它可以冻结/挂起,然后继续/恢复。不要再从线程和信号量的Angular 考虑协程,而是从continuation甚至回调钩子的Angular 考虑协程。
为了清楚起见,协程可以通过使用
suspend
函数来暂停。让我们研究一下:在Android中,我们可以这样做:
上面的代码将打印以下内容:
想象它是这样工作的:
所以你启动的当前函数不会停止,只是协程在继续的时候会挂起。线程不会因为运行挂起函数而暂停。
我想你把事情说清楚,是我的参考。
让我们做一些很酷的事情,在迭代的中间冻结suspend函数。
存储一个名为
continuation
的变量,我们将用协程continuation对象加载它:现在,让我们返回到挂起的函数,并使其在迭代过程中冻结:
然后在其他地方像onResume(例如):
循环将继续。知道我们可以在任何时候冻结一个suspend函数并在beeb经过一段时间后恢复它,这是非常简洁的。
nzrxty8p6#
这里有很多很好的答案,但我认为还有两件事值得注意。
launch / withContext / runBlocking和例子中的很多其他东西都来自于协程库。这实际上与suspend无关。你不需要协程库来使用协程。协程是编译器的一个“技巧”。是的,库确实让事情变得更容易,但是编译器正在做暂停和恢复事情的魔术。
第二件事是,编译器只是将看起来像过程的代码转换为后台的回调。
以下面这个挂起的不使用协程库的最小协程为例:
我认为理解它的一个重要方法是看看编译器对这段代码做了什么。有效地,它为lambda创建了一个类。它为“额外”字符串在类中创建了一个属性,然后它创建了两个函数,一个打印“before”,另一个打印“after”。
实际上,编译器将看起来像过程代码的代码转换为回调。
那么
suspend
关键字是做什么的呢?它告诉编译器在生成的回调中需要多久才能找到上下文。编译器需要知道哪些变量在哪些“回调”中使用,suspend关键字帮助了它。在这个例子中,“extra”变量在suspend之前和之后都使用。因此,需要将其提取到包含编译器所做回调的类的属性中。它还告诉编译器这是状态的“开始”,并准备将以下代码分割为回调。
startCoroutine
只存在于suspend lambda中。Kotlin编译器生成的实际Java代码在这里。它是一个switch语句而不是回调,但实际上是一样的。首先调用w/ case 0,然后在resume之后调用w/ case 1。
w46czmvw7#
假设我们有一个名为myFunction的函数。
通常,这些代码块的执行方式类似于block1、block2、block3、block4。因此,代码块3和4可能会在代码块2仍在运行时执行。由于这个原因,可能会出现问题。(屏幕可能冻结,应用程序可能会崩溃)
但是如果我们让这个函数暂停
现在,这个函数可以在代码块2(长时间运行的操作)开始执行时暂停,并在完成后恢复。代码块3和4将在此之后执行。因此不会出现意外的线程共享问题。
dgenwo3n8#
对于那些仍然想知道如何实际挂起suspend函数的人,我们在suspend函数的主体中使用suspendCoroutine函数。
cfh9epnr9#
挂起函数是所有协程的核心。挂起函数就是一个可以暂停并在稍后恢复的函数。它们可以执行一个长时间运行的操作,并等待它完成而不会阻塞。
挂起函数的语法与常规函数类似,只是增加了
suspend
关键字。它可以接受一个参数并具有一个返回类型。但是,挂起函数只能由另一个挂起函数或在协程中调用。在底层,suspend函数被编译器转换为另一个没有suspend关键字的函数,它接受一个
Continuation<T>
类型的附加参数。例如,上面的函数将被编译器转换为:Continuation<T>
是一个包含两个函数的接口,调用这两个函数可以通过返回值或异常恢复协程,如果函数挂起时发生错误。