在 Kotlin 协程中,suspend 函数是什么意思?

287
我正在阅读 Kotlin Coroutine,并了解它是基于 "suspend" 函数的。但是 "suspend" 是什么意思呢?
协程或函数可以被暂停吗?
根据 Kotlin 官方文档:
基本上,协程是可以在不阻塞线程的情况下暂停的计算。
我听人们经常说 "suspend 函数"。但我认为被暂停的是协程,因为它在等待函数执行完毕。在这种情况下,"suspend" 通常意味着 "停止操作",也就是协程处于空闲状态。
我们应该说协程被暂停了吗?
哪个协程会被暂停?
根据 Kotlin 官方文档:
继续类比,await() 可以是一个暂停函数(因此也可从 async {} 块内调用),它会暂停协程直到某个计算完成并返回结果。
async { // Here I call it the outer async coroutine
    ...
    // Here I call computation the inner coroutine
    val result = computation.await()
    ...
}

它说“暂停协程直到某个计算完成”,但协程就像是轻量级的线程。所以如果协程被暂停了,那么计算怎么能完成呢?
我们看到在`computation`上调用了`await`,所以它可能是返回`Deferred`的`async`函数,这意味着它可以启动另一个协程。
fun computation(): Deferred<Boolean> {
    return async {
        true
    }
}

在引用中提到了“暂停协程”。这是指暂停外部的“async”协程,还是暂停内部的“computation”协程呢?
“暂停”是否意味着当外部的“async”协程在等待内部的“computation”协程完成时,它(即外部的“async”协程)会空闲下来(因此被称为暂停),将线程返回给线程池,并且当子“computation”协程完成时,它(即外部的“async”协程)会唤醒,从线程池中获取另一个线程并继续执行?
我提到线程的原因是因为:https://kotlinlang.org/docs/tutorials/coroutines-basic-jvm.html 引用如下: “当协程等待时,线程会返回到线程池中,等待完成后,协程会在线程池中的空闲线程上恢复执行。”

建议学习 Kotlin 基础知识,详情请参考 Kotlin 基础 - Pragnesh Ghoda シ
不 @PragneshGhodaシ - Nora Söderlund
10个回答

324
挂起函数是协程的核心。挂起函数就是一种可以在稍后暂停和恢复执行的函数。它们可以执行长时间运行的操作并等待其完成,而不会阻塞程序。
挂起函数的语法与普通函数类似,只是加了一个 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)
}

68
我想知道这个函数是如何被暂停的?他们总是说suspend fun可以被暂停,但具体是怎样实现的呢? - WindRider
9
@WindRider 这句话的意思是当前线程开始执行其他协程,稍后会返回执行这个协程。 - Joffrey
19
我已经弄清楚了这个“神秘”的机制。只需使用 工具 > Kotlin > 字节码 > 反编译 按钮,就可以轻松揭示它。它显示了所谓的“挂起点”是如何通过Continuation等实现的。任何人都可以自己看一眼。 - WindRider
10
@buzaa 这里有一个2017年Roman Elizarov的讲座,讲解了字节码级别的内容。 - Marko Topolnik
13
根据这个解释,似乎您可以在“长时间运行的操作”中放置任何内容,线程会在任何它认为合适的地方暂停执行。但这并不准确。从悬挂函数内调用阻塞函数仍将阻塞它正在运行的线程。悬挂函数将运行,如果它找到另一个悬挂函数调用,它将保存其状态,并释放线程以运行调用堆栈中的下一个任务。当该任务完成后,原始函数将恢复执行。无论如何,那只是我对它的理解:https://youtu.be/BOHK_w09pVA?t=577 - Miguel Lasa
显示剩余4条评论

126
“但是暂停是什么意思呢?”
使用 “suspend” 关键字标记的函数在编译时会转换为异步函数,并且在字节码中实现,尽管它们在源代码中看起来是同步的。
我认为理解这种转换的最好来源是 Roman Elizarov 的演讲 "深入浅出协程"
例如,这个函数:
class MyClass {
    suspend fun myFunction(arg: Int): String {
        delay(100)
        return "bob"
    }
}

被转换为(为简单起见,使用Java表达而不是实际的JVM字节码):

public final class MyClass {
    public final Object myFunction(int arg, @NotNull Continuation<? super String> $completion) {
        // body turned into a state machine, hidden for brevity
    }
}

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

  • 返回类型更改为Java的Object(相当于Kotlin的Any? - 包含所有值的类型),以允许返回一个特殊的COROUTINE_SUSPENDED令牌,表示协程实际上已暂停
  • 它获得了一个额外的Continuation<X>参数(其中X是在代码中声明的函数的先前返回类型 - 在示例中为String)。当恢复挂起函数时,此继续作为回调。
  • 其主体被转换为状态机(而不是直接使用回调,以提高效率)。这是通过将函数的主体分解为围绕所谓的挂起点的部分,并将这些部分转换为大开关的分支来完成的。有关局部变量和我们在开关中的位置的状态存储在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. we're back from the call to computation, about to call await()
    //    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. With multiple threads,
    //    C2 may have already started executing. In any case we wait 
    //    here for C2 to complete.
    // 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()的调用挂起了外部async协程的执行,直到内部 async的协程执行完毕。

即使使用单线程也可以看到这一点:线程将执行外部async的开始,然后调用computation()并达到内部async。此时,内部异步体的主体被跳过,线程继续执行外部async,直到它达到await()await()是一个"挂起点",因为await是一个挂起函数。 这意味着外部协程被挂起,因此线程开始执行内部协程。当它完成时,它回来执行外部async的结尾。

“suspend” 的意思是,当外部异步协程在等待(await)内部计算协程完成时,它(外部异步协程)会空闲下来(因此称为“暂停”),并将线程返回给线程池;当子计算协程完成时,它(外部异步协程)会唤醒,从线程池中获取另一个线程并继续执行?
是的,完全正确。
实际上,这是通过将每个暂停函数转换为状态机来实现的,其中每个“状态”对应于该暂停函数内部的暂停点。在底层,可以多次调用该函数,并提供有关应从哪个暂停点开始执行的信息(您真的应该观看我链接的视频以获取更多信息)。

21
很棒的回答,当涉及到协程时,我很想念那种非常基础的解释。 - bernardo.g
好的解释。我的理解是 - 在多线程/多核机器的情况下,协程可以在其他线程上并行运行,也可以在同一线程上同时运行,并且调度点将是调用挂起函数的行,而挂起点将是await()行。我的理解是正确的吗,@Joffrey?是否有任何想法,关于它如何决定启动另一个线程或继续在同一个线程上,并且新线程的成本是多少,以及如何优化以减少具有大量长时间运行协程的代码的上下文切换的成本? - Siva S
这应该是被接受的答案。它直接回答了原帖中的两个问题。 - Sergio Rodriguez
@GenaBatsyan,并非所有的挂起函数都可以取消,因此这个测试并没有显示没有悬停点,而是一个只有打印语句的挂起函数是不可取消的。我尝试反编译了您描述的代码,并且函数A确实有一个状态机,在调用函数B时有一个悬停点。 - Joffrey
1
经过进一步调查,让我澄清一下。更准确地说,在函数A中存在一个暂停点,这意味着在调用之前和之后,该函数被分解为由状态机驱动的部分,并且可以处理COROUTINE_SUSPENDED值。但是,这个特定的函数B从未实际暂停(它从不返回COROUTINE_SUSPENDED),因此函数A也从未暂停过(因为它需要接收该值),因此调度程序从未有机会在那一点停止执行协程。 - Joffrey
显示剩余6条评论

51

为了理解什么是挂起协程,我建议您查看以下代码:

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 调用的一部分。过程如下:

  1. 评估 val a = a()
  2. 这将链接到 b(),达到 suspendCoroutine
  3. 函数 b() 执行传递给 suspendCoroutine 的块,然后返回一个特殊的 COROUTINE_SUSPENDED 值。这个值在 Kotlin 编程模型中不可观察,但编译后的 Java 方法就是这样做的。
  4. 函数 a() 看到此返回值,也会将其返回。
  5. launch 块执行同样的操作,现在控制权返回到 launch 调用之后的行:10.downTo(0)...

请注意,此时您的 fun main 代码与 launch 块内的代码具有相同的效果,就好像它们同时执行一样。只是所有这些都发生在单个本机线程上,所以 launch 块被“暂停”。

现在,在 forEach 循环代码内部,程序读取 b() 函数编写的 continuation 并使用值 10 resume 它。 resume() 实现的方式是,就好像 suspendCoroutine 调用返回您传递的值一样。因此,您突然发现自己正在执行 b() 的中间。您传递给 resume() 的值被分配给 i 并与 0 进行比较。如果不为零,则在 b() 中继续进行 while (true) 循环,再次到达 suspendCoroutine,此时您的 resume() 调用返回,并且现在您会通过 forEach() 再次进行循环步骤。这将一直持续,直到最终您恢复了 0,然后 println 语句运行,程序完成。

上述分析应该让您重要的直觉认识到,“挂起协程”意味着将控制权返回到最内层的 launch 调用(或更一般地,协程构建器)。如果一个协程在恢复后再次暂停,则 resume() 调用将结束,并且控制权将返回到 resume() 的调用者。

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


除了"unconfined"(未限制的)外,其他调度程序基本上会在resume()中立即返回。 - Paul Stelian
1
非常好的例子和解释! - konunger

41

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

runBlocking 的用例:

  • myMethod() is suspend function
  • runBlocking { } starts a Coroutine in blocking way. It is similar to how we were blocking normal threads with Thread class and notifying blocked threads after certain events.
  • runBlocking { } does block the current executing thread, until the coroutine (body between {}) gets completed

     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 { } starts a coroutine concurrently.
  • This means that when we specify launch, a coroutine starts execution on worker thread.
  • The worker thread and outer thread (from which we called launch { }) both runs concurrently. Internally, JVM may perform Preemptive Threading
  • When we require multiple tasks to run in parallel, we can use this. There are scopes which specify lifetime of coroutine. If we specify GlobalScope, the coroutine will work until application lifetime ends.

    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 的应用场景:

  • When we have multiple tasks to do and they depend on other's completion, async and await would help.
  • For example, in below code, there are 2 suspend functions myMethod() and myMethod2(). myMethod2() should get executed only after full completion of myMethod() OR myMethod2() depends on result of myMethod(), we can use async and await
  • async starts a coroutine in parallel similar to launch. But, it provides a way to wait for one coroutine before starting another coroutine in parallel.
  • That way is await(). async returns an instance of Deffered<T>. T would be Unit for default. When we need to wait for any async's completion, we need to call .await() on Deffered<T> instance of that async. Like in below example, we called innerAsync.await() which implies that the execution would get suspended until innerAsync gets completed. We can observe the same in output. The innerAsync gets completed first, which calls myMethod(). And then next async innerAsync2 starts, which calls 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

感谢您描述使用案例。 - ʕ ᵔᴥᵔ ʔ
1
这是最全面和简单的示例,可以让新手了解协程和线程的工作原理!感谢Kushal! - user2498079

14

我希望给你举个简单的延续概念例子。一个挂起函数可以冻结/暂停,然后恢复/继续。不要再按照线程和信号量来思考协程。考虑到延续和回调钩子。

清楚了,使用 suspend 函数就可以暂停协程。让我们来看一个例子,在安卓里:

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

想象它的工作方式:

enter image description here

所以你从中启动的当前函数不会停止,只有协程会暂停而继续执行。通过运行挂起函数来暂停线程。

我认为这个网站可以帮助你理清思路,并且是我的参考。

让我们做一些酷炫的事情,在迭代过程中冻结我们的挂起函数。我们稍后将在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()
    }

循环将继续执行。了解我们可以冻结任何时候的挂起函数并在一段时间后恢复它非常不错。你也可以了解通道


12
我发现理解的最好方法是将this关键字和coroutineContext属性进行类比。
Kotlin函数可以声明为局部或全局。局部函数神奇地访问this关键字,而全局函数则没有。
Kotlin函数可以声明为或阻止。 函数神奇地访问coroutineContext属性,而阻止函数则没有。
问题在于:coroutineContext属性在Kotlin stdlib中像“普通”属性一样声明,但是这个声明只是文档/导航目的的存根。 实际上,coroutineContext内置的内部属性, 这意味着在编译器魔法下,它了解此属性,就像它了解语言关键字一样。
对于局部函数,this关键字的作用就像对于函数的coroutineContext属性的作用:它提供了对执行当前上下文的访问权限。 因此,需要才能访问coroutineContext属性 - 当前正在执行的协程上下文的实例

5
这里有很多很好的答案,但我认为还有两件重要的事情需要注意。 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”字符串属性,然后创建了两个函数,一个打印“before”,另一个打印“after”。

实际上,编译器将看起来像过程性代码的内容转换为回调。

那么,“suspend”关键字是做什么的呢?它告诉编译器需要向后查找多远以获取生成的回调所需的上下文。编译器需要知道哪些变量在哪些“回调”中使用,并且suspend关键字有助于它完成这项任务。在这个例子中,“extra”变量在suspend之前和之后都被使用。因此,它需要被提取到包含编译器生成的回调的类的属性中。

它还告诉编译器这是状态的“开始”,并准备将以下代码拆分为回调函数。只有在暂停的lambda函数上才存在“startCoroutine”。

Kotlin编译器生成的实际Java代码在此处。它是一个switch语句而不是回调函数,但实际上是相同的。首次调用时为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");
            }

这是一个很好的解释。 - Tushar Saha

4
在 Kotlin 协程中,suspend 函数是用于标记函数为 coroutine 的关键字。一个 coroutine 是一种轻量级的类似线程的结构,它允许异步或长时间运行的操作按顺序执行而不会阻塞主线程。
当一个函数被标记为 suspend 时,意味着该函数可以在某些点暂停其执行而不会阻塞线程,从而允许其他协程运行。这些暂停通常发生在等待异步操作完成时,例如网络请求或磁盘 I/O。以下是使用 suspend 关键字的函数示例:
suspend fun fetchData(): String {
    delay(1000) // Simulating a delay for demonstration purposes [it also suspend function]
    return "Data fetched!"
}

挂起函数是做什么用的?

1- pause/resume 

2- save all variables (save the progress of function when function is pause and resume)

你需要调用挂起函数吗?
you need a coroutines scope to call suspend function or another suspend function 

类型协程作用域

1 - lifecycleScope -> (与activity配合使用) = 感知activity生命周期

2 - viewModelScope -> (与viewModel配合使用) = 感知viewModel生命周期

3 - GlobalScope -> 与应用程序作用域一起使用

4 - runBlocking -> (不建议使用,因为会阻塞线程)

5 - 更多...

那么我该如何使用Kotlin调用挂起函数呢? 在第5个线程中启动它。

1 - 主线程 -> 用于UI

2 - IO线程 -> 用于输入输出

3- 默认线程

4 - 未指定线程

**5 - 自定义线程 -> 你创建的线程**

要调用挂起函数,需要3件事

first you need `builder` (Scope)
second `Dispatchers` (for any thread you will work) [Optional]
third `body of scope` you can call suspend here

让我们举个例子

fun main(){
   GlobalScope.launch(Dispatchers.IO/*[Optional]*/){
      // this is body 
      for(i in 1..10){
        Log.i("TEST", fetchData())   // open logcat to see the result           
      }
   }
}

当你完成协程后,它将返回到主线程。

3
假设我们有一个名为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这样进行的。因此,当代码块2仍在运行时,代码块3和4可能会执行。由于这个原因,可能会出现问题(屏幕可能会冻结,应用程序可能会崩溃)。
但如果我们将这个函数暂停。
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。因此,不会出现意外的线程共享问题。


在不使用挂起函数的示例中,当块2未完成时,如何执行块3和块4? - avishak chakroborty

0

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

1
我不知道你想通过这段代码实现什么,但是suspendCoroutine主要用于一些带有回调的旧代码。在这里使用suspendCoroutine的意义是什么?它不会将上下文切换到后台线程,因此它将阻塞协程运行的线程。如果协程使用Dispatchers.Main上下文,则会阻塞主线程。 - Sergio

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接