Kotlin 协程中的挂起函数是啥意思?

Posted

技术标签:

【中文标题】Kotlin 协程中的挂起函数是啥意思?【英文标题】:What does the suspend function mean in a Kotlin Coroutine?Kotlin 协程中的挂起函数是什么意思? 【发布时间】:2018-05-31 23:55:35 【问题描述】:

我正在阅读 Kotlin Coroutine 并且知道它基于 suspend 函数。但是suspend 是什么意思?

协程或函数被挂起?

来自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()
    ...

它说“暂停协程直到完成某些计算”,但协程就像一个轻量级线程。那么如果协程被挂起,如何进行计算呢?

我们看到computation上调用了await,所以可能是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

协程等待时线程返回池中,等待完成后,协程在池中的空闲线程上恢复

【问题讨论】:

【参考方案1】:

要了解挂起协程的确切含义,我建议您阅读以下代码:

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(),看到这个返回值,自己也返回。 launch 块执行相同的操作,现在控制返回到launch 调用之后的行:10.downTo(0)...

请注意,此时,您的效果与launch 块内的代码和您的fun main 代码同时执行的效果相同。碰巧这一切都发生在一个本地线程上,所以 launch 块被“暂停”。

现在,在forEach 循环代码中,程序读取b() 函数写入的continuationresumes 其值为10resume() 的实现方式就像suspendCoroutine 调用返回了您传入的值一样。所以您突然发现自己正处于执行b() 的中间。您传递给resume() 的值被分配给i 并与0 核对。如果它不为零,则while (true) 循环在b() 内部继续,再次到达suspendCoroutine,此时您的resume() 调用返回,现在您在forEach() 中执行另一个循环步骤。这一直持续到最后你用0 继续,然后println 语句运行并且程序完成。

上面的分析应该给你一个重要的直觉,“暂停协程”意味着将控制权返回到最里面的launch调用(或者,更一般地说,协程构建器)。如果协程恢复后再次挂起,resume() 调用结束,控制权返回给resume() 的调用者。

协程调度器的存在使这种推理变得不那么清晰,因为它们中的大多数会立即将您的代码提交给另一个线程。在这种情况下,上面的故事发生在另一个线程中,协程调度器还管理continuation 对象,因此它可以在返回值可用时恢复它。

【讨论】:

除了不受限制的调度程序之外,基本上都是立即从 resume() 返回的,差不多。【参考方案2】:

挂起函数是所有协程的核心。 挂起函数只是一个可以在以后暂停和恢复的函数。他们可以执行长时间运行的操作并等待它完成而不会阻塞。

除了添加了suspend 关键字之外,挂起函数的语法类似于常规函数的语法。它可以接受一个参数并有一个返回类型。但是,挂起函数只能由另一个挂起函数或在协程内调用。

suspend fun backgroundTask(param: Int): Int 
     // long running operation

在后台,挂起函数被编译器转换为另一个没有挂起关键字的函数,它接受Continuation&lt;T&gt; 类型的附加参数。例如上面的函数,编译器会转换成这个:

fun backgroundTask(param: Int, callback: Continuation<Int>): Int 
   // long running operation

Continuation&lt;T&gt; 是一个包含两个函数的接口,它们被调用以通过返回值或在函数挂起时发生错误时出现异常来恢复协程。

interface Continuation<in T> 
   val context: CoroutineContext
   fun resume(value: T)
   fun resumeWithException(exception: Throwable)

【讨论】:

我想知道这个功能实际上是如何暂停的?他们总是说suspend fun 可以暂停,但具体怎么暂停呢? @WindRider 只是表示当前线程开始执行其他一些协程,稍后会回到这个。 我发现了“神秘”的机制。它可以在 Tools > Kotlin > Bytecode > Decompile btn 的帮助下轻松揭开。它显示了所谓的“暂停点”是如何实现的——通过继续等等。大家可以自己去看看。 @buzaa 这是 Roman Elizarov 于 2017 年撰写的 talk,它在字节码级别进行了解释。 通过这种解释,听起来你可以在long running operation 中放入任何东西,线程将在它认为合适的任何地方暂停执行。这听起来不准确。从挂起函数内部调用阻塞函数仍然会阻塞它正在运行的线程。挂起函数将运行,如果它发现另一个挂起函数调用,它将保存其状态,并释放线程以供调用堆栈中的下一个任务运行。当该任务完成时,原始功能恢复。反正我就是这么理解的:youtu.be/BOHK_w09pVA?t=577【参考方案3】:

但是暂停是什么意思呢?

suspend 关键字标记的函数在编译时被转换为在后台异步,即使它们在源代码中看起来是同步的。

了解这种转变 IMO 的最佳来源是 Roman Elizarov 的演讲 "Deep Dive into Coroutines"。

这包括对函数的以下更改:

返回类型改为Unit,这就是Kotlin表示void函数的方式 它获得一个额外的Continuation&lt;X&gt; 参数(其中X 是在代码中声明的函数的前一个返回类型)。这种延续就像一个回调。 它的主体变成了一个状态机(而不是字面上使用回调,以提高效率)。这是通过将函数的主体分解成所谓的悬挂点周围的部分,然后将这些部分变成一个大开关的分支来完成的。有关局部变量的状态以及我们在开关中的位置存储在 Continuation 对象中。

这是一种非常快速的描述方式,但您可以通过更多细节和演讲中的示例看到它。这整个转换基本上就是“挂起/恢复”机制是如何在后台实现的。

协程或函数被挂起?

在高层次上,我们说调用一个suspending函数suspends协程,意味着当前线程可以开始执行另一个协程。因此,协程被称为被挂起而不是函数。

实际上,挂起函数的调用点因此被称为“挂起点”。

哪个协程被挂起?

让我们看看你的代码并分解发生了什么:

// 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() 的调用暂停outerasync 协程的执行,直到innerasync 的协程执行结束。 p>

你甚至可以看到单线程:线程将执行外部async 的开头,然后调用computation() 并到达内部async。此时,内部异步的主体被跳过,线程继续执行外部async,直到到达await()await() 是一个“暂停点”,因为await 是一个暂停函数。 这意味着外部协程被挂起,因此线程开始执行内部协程。完成后,返回执行外部async的结尾。

挂起是否意味着当外部异步协程正在等待(等待)内部计算协程完成时,它(外部异步协程)空闲(因此称为挂起)并将线程返回到线程池,并且当子计算协程完成,它(外部异步协程)唤醒,从池中获取另一个线程并继续?

是的,没错。

实际实现的方法是将每个挂起函数变成一个状态机,其中每个“状态”对应于该挂起函数内部的一个挂起点。在后台,可以多次调用该函数,并提供有关它应该从哪个暂停点开始执行的信息(您应该真正观看我链接的视频以获取更多信息)。

【讨论】:

很好的答案,我想念关于协程的那种非常基本的解释。 为什么没有用任何其他语言实现?还是我错过了什么?我一直在考虑这个解决方案,很高兴 Kotlin 有它,但不确定为什么 TS 或 Rust 有这样的东西 @PEZO 很好的协程已经存在了很长时间。 Kotlin 没有发明它们,但语法和库使它们大放异彩。 Go 有 goroutine,javascript 和 TypeScript 有 Promise。唯一的区别在于使用它们的语法细节。我发现以这种方式标记 JS 的 async 函数但仍然返回 Promise 非常烦人/令人不安。 对不起,我的评论不清楚。我指的是suspend关键字。它与异步不同。 经过进一步调查,让我澄清一下。更准确地说,函数 A 中有一个暂停点,即函数在调用之前和之后被分解为多个部分,由状态机驱动,并且可以处理 COROUTINE_SUSPENDED 值。但是,这个特定的函数 B 永远不会实际上暂停(它永远不会返回 COROUTINE_SUSPENDED),所以函数 A 也永远不会暂停(因为它需要接收该值),所以调度程序永远没有机会停止执行协同程序。【参考方案4】:

我想给你一个关于延续概念的简单例子。这就是挂起函数的作用,它可以冻结/挂起,然后继续/恢复。不要再从线程和信号量的角度考虑协程了。从延续甚至回调挂钩的角度来考虑它。

需要明确的是,可以使用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

想象一下它是这样工作的:

所以你启动的当前函数不会停止,只是一个协程会在它继续时暂停。线程不会通过运行挂起函数来暂停。

我认为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

【讨论】:

【参考方案5】:

我发现理解suspend 的最佳方法是在this 关键字和coroutineContext 属性之间进行类比。

Kotlin 函数可以声明为本地或全局。本地函数可以神奇地访问this 关键字,而全局则不能。

Kotlin 函数可以声明为 suspend 或阻塞。 suspend 函数神奇地可以访问 coroutineContext 属性,而阻塞函数则不能。

事情是:coroutineContext 属性 is declared like a "normal" property 在 Kotlin 标准库中,但此声明只是用于文档/导航目的的存根。事实上,coroutineContext 是 builtin intrinsic property,这意味着在引擎盖下,编译器可以像知道语言关键字一样知道这个属性。

this 关键字对本地函数的作用与 coroutineContext 属性对 suspend 函数的作用相同:它提供对当前执行上下文的访问权限。

所以,您需要suspend 才能访问coroutineContext 属性 - 当前执行的协程上下文的实例

【讨论】:

【参考方案6】:

由于已经有很多好的答案,我想为其他人发布一个更简单的示例。

runBlocking 用例:

myMethod() 是suspend 函数 runBlocking 以阻塞方式启动协程。这类似于我们使用Thread 类阻塞普通线程并在某些事件后通知阻塞线程的方式。

runBlocking 阻塞当前正在执行的线程,直到协程( 之间的主体)完成

 override fun onCreate(savedInstanceState: Bundle?) 
    super.onCreate(savedInstanceState)
    setContentView(R.layout.main_activity)
    Log.i(TAG,"Outer code started on Thread : " + Thread.currentThread().name);
    runBlocking 
        Log.d(TAG,"Inner code started  on Thread : " + Thread.currentThread().name + " making outer code suspend");
        myMethod();
    
    Log.i(TAG,"Outer code resumed on Thread : " + 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 同时启动一个协程。 这意味着当我们指定启动时,协程会在worker 线程上开始执行。 worker 线程和外线程(我们称之为launch )都同时运行。在内部,JVM 可能会执行 抢占式线程

当我们需要多个任务并行运行时,我们可以使用它。有 scopes 指定协程的生命周期。如果我们指定GlobalScope,协程将一直工作到应用程序生命周期结束。

override fun onCreate(savedInstanceState: Bundle?) 
    super.onCreate(savedInstanceState)
    setContentView(R.layout.main_activity)
    Log.i(TAG,"Outer code started on Thread : " + Thread.currentThread().name);

    GlobalScope.launch(Dispatchers.Default) 
        Log.d(TAG,"Inner code started  on Thread : " + Thread.currentThread().name + " making outer code suspend");
        myMethod();
    
    Log.i(TAG,"Outer code resumed on Thread : " + 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

asyncawait 用例:

当我们有多项任务要完成并且他们依赖于其他人的完成时,asyncawait 会有所帮助。 例如,在下面的代码中,有2挂起函数myMethod()和myMethod2()。 myMethod2() 应该在 myMethod() 完全完成后执行 OR myMethod2() 取决于 myMethod() 的结果,我们可以使用 asyncawait async 启动一个与launch 类似的并行协程。但是,它提供了一种在并行启动另一个协程之前等待一个协程的方法。

那是await()async 返回Deffered&lt;T&gt; 的实例。 T 默认为 Unit。当我们需要等待任何async 完成时,我们需要在该asyncDeffered&lt;T&gt; 实例上调用.await()。就像在下面的示例中一样,我们调用了innerAsync.await(),这意味着执行将暂停,直到innerAsync 完成。我们可以在输出中观察到相同的情况。 innerAsync 首先完成,它调用myMethod()。然后下一个async innerAsync2 开始,它调用myMethod2()

override fun onCreate(savedInstanceState: Bundle?) 
    super.onCreate(savedInstanceState)
    setContentView(R.layout.main_activity)
    Log.i(TAG,"Outer code started on Thread : " + Thread.currentThread().name);

     job = GlobalScope.launch(Dispatchers.Default) 
         innerAsync = async 
             Log.d(TAG, "Inner code started  on Thread : " + Thread.currentThread().name + " making outer code suspend");
             myMethod();
         
         innerAsync.await()

         innerAsync2 = async 
             Log.w(TAG, "Inner code started  on Thread : " + Thread.currentThread().name + " making outer code suspend");
             myMethod2();
         
    

    Log.i(TAG,"Outer code resumed on Thread : " + 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);
        
    


private suspend fun myMethod2() 
    withContext(Dispatchers.Default) 
        for(i in 1..10) 
            Log.w(TAG,"Inner code i : $i on Thread : " + Thread.currentThread().name);
        
    

这个输出:

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

【讨论】:

感谢您描述用例。 这是一个最全面、最简单的例子,让新人了解协程和线程的工作原理!谢谢库沙尔!【参考方案7】:

这里有很多很棒的答案,但我认为还有两点需要注意。

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 只存在于suspend lambda。

Kotlin 编译器生成的实际 Java 代码在这里。这是一个 switch 语句而不是回调,但实际上是同一件事。先调用 case 0,然后在 resume 后调用 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");
            

【讨论】:

【参考方案8】:

任何人仍然绊倒这个问题,我建议快速浏览一下。我已经阅读了很多关于这个问题的误导性答案,甚至一些评价最高。这消除了我的很多疑虑。

https://youtu.be/BOHK_w09pVA?t=577

【讨论】:

那个视频是一流的!我必须“在旅途中”做一些 Kotlin,所以我正在尽可能快地学习这门语言,LiveData 和暂停之类的东西看起来非常重要,但缺乏官方文档。该视频完美地解释了它们的意义【参考方案9】:

对于仍然想知道如何实际挂起挂起函数的任何人,我们在挂起函数的主体中使用 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

【讨论】:

以上是关于Kotlin 协程中的挂起函数是啥意思?的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin 协程协程的挂起和恢复 ② ( 协程挂起 和 线程阻塞 对比 )

Kotlin 协程Flow 异步流 ① ( 以异步返回返回多个返回值 | 同步调用返回多个值的弊端 | 尝试在 sequence 中调用挂起函数返回多个返回值 | 协程中调用挂起函数返回集合 )

Kotlin 协程Flow 异步流 ① ( 以异步返回返回多个返回值 | 同步调用返回多个值的弊端 | 尝试在 sequence 中调用挂起函数返回多个返回值 | 协程中调用挂起函数返回集合 )

你应该知道的协程中的挂起转化小技巧

你应该知道的协程中的挂起转化小技巧

Kotlin 协程协程的挂起和恢复 ① ( 协程的挂起和恢复概念 | 协程的 suspend 挂起函数 )