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

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


当前回答

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

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

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

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

其他回答

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

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

例子中的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()
    }

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