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

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


当前回答

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

其他回答

但是暂停是什么意思呢?

用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)内部计算协程完成时,它(外部异步协程)空闲(因此称为挂起)并返回线程池,当子计算协程完成时,它(外部异步协程)醒来,从池中取出另一个线程并继续?

是的,准确。

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

这里有很多很好的答案,但我认为还有两件重要的事情需要注意。

例子中的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”字符串创建了一个属性,然后创建了两个函数,一个打印“之前”,另一个打印“之后”。

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

那么,suspend关键字是做什么的呢?它告诉编译器要回溯到多远的地方去寻找生成的回调所需要的上下文。编译器需要知道在哪个“回调”中使用了哪些变量,而suspend关键字可以帮助它。在这个例子中,“extra”变量在挂起之前和之后都被使用。因此,它需要被拉出到包含编译器所做回调的类的属性中。

它还告诉编译器这是状态的“开始”,并准备将下面的代码分解为回调。startCoroutine只存在于挂起lambda上。

Kotlin编译器生成的实际Java代码在这里。这是一个switch语句而不是回调,但实际上是一样的。首先调用w/ case 0,然后在恢复后调用w/ 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将在此之后执行。因此不会出现意外的线程共享问题。

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

暂停函数的语法与常规函数相似,只是增加了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)
}

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

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

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

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