我正在阅读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

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


当前回答

为了理解挂起协程到底意味着什么,我建议你看一下下面的代码:

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()的调用者。

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

其他回答

为了理解挂起协程到底意味着什么,我建议你看一下下面的代码:

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)
}

但是暂停是什么意思呢?

用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) {
        // ...
    }
}

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

The return type is changed to Java's Object (the equivalent of Kotlin's Any? - a type containing all values), to allow returning a special COROUTINE_SUSPENDED token to represent when the coroutine is actually suspended It gets an additional Continuation<X> argument (where X is the former return type of the function that was declared in the code - in the example it's String). This continuation acts like a callback when resuming the suspend function. Its body is turned into a state machine (instead of literally using callbacks, for efficiency). This is done by breaking down the body of the function into parts around so called suspension points, and turning those parts into the branches of a big switch. The state about the local variables and where we are in the switch is stored inside the Continuation object.

这是一种非常快速的描述方式,但是你可以在演讲中看到更多的细节和例子。整个转换基本上就是“挂起/恢复”机制在底层的实现方式。

协程或函数被挂起?

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

事实上,由于这个原因,暂停函数的调用点被称为“暂停点”。

哪个协程被挂起?

让我们看看你的代码,并分解发生了什么(编号遵循执行时间轴):

// 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)
    }
}

外部异步启动一个协程。当它调用computation()时,内部异步程序启动第二个协程。然后,await()调用挂起外部异步协程的执行,直到内部异步协程的执行结束。

You can even see that with a single thread: the thread will execute the outer async's beginning, then call computation() and reach the inner async. At this point, the body of the inner async is skipped, and the thread continues executing the outer async until it reaches await(). await() is a "suspension point", because await is a suspending function. This means that the outer coroutine is suspended, and thus the thread starts executing the inner one. When it is done, it comes back to execute the end of the outer async.

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

是的,准确。

实现这一点的实际方法是将每个挂起函数转换为一个状态机,其中每个“状态”对应于这个挂起函数中的一个挂起点。在底层,函数可以被多次调用,并带有关于它应该从哪个挂起点开始执行的信息(您应该真正地观看我链接的视频以获得有关这方面的更多信息)。

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

需要明确的是,协程可以通过使用挂起函数暂停。让我们来研究一下:

在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

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

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

我认为这个网站可以帮助你把事情弄清楚,是我的参考。

让我们做一些很酷的事情,在迭代过程中冻结我们的挂起函数。稍后我们将在onResume中恢复它

存储一个名为continuation的变量,我们将用coroutines 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()
    }

循环将继续。我们可以在任何时候冻结一个挂起函数,并在经过一段时间后恢复它。你也可以查看频道

假设我们有一个名为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将在此之后执行。因此不会出现意外的线程共享问题。