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

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


当前回答

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

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

其他回答

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

我发现理解挂起的最好方法是在这个关键字和coroutineContext属性之间做一个类比。

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

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

它是:coroutineContext属性 在Kotlin stdlib中像“正常”属性一样声明,但此声明只是用于文档/导航目的的存根。事实上,coroutineContext是一个内置的内在属性,这意味着在编译器的魔法下,它知道这个属性,就像它知道语言关键字一样。

这个关键字对局部函数的作用与coroutineContext属性对挂起函数的作用相同:它提供了对当前执行上下文的访问。

因此,您需要挂起以获得对coroutineContext属性的访问—当前执行的协程上下文的实例

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

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

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

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

在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将在此之后执行。因此不会出现意外的线程共享问题。