什么是暂停功能在Kotlin Coroutine中意味着什么

时间:2017-12-18 15:47:32

标签: kotlin async-await suspend kotlin-coroutines

我正在阅读Kotlin Coroutine并知道它基于suspend功能。但suspend是什么意思?

协程或功能被暂停?

来自https://kotlinlang.org/docs/reference/coroutines.html

  

基本上,协同程序是可以在不阻塞线程的情况下暂停的计算

我听说人们经常说"暂停功能"。但是我认为正在等待函数完成的是协同程序被暂停? "暂停"通常意味着"停止操作",在这种情况下,协程是空闲的。

我们应该说协程被暂停吗?

哪个协程被暂停?

来自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上看到computation被调用,因此可能async返回Deferred,这意味着它可以启动另一个协程

fun computation(): Deferred<Boolean> {
    return async {
        true
    }
}

引用说明 暂停协程 。这是suspend外部async协程,还是suspend内部computation协程?

suspend是否意味着外部async协程正在等待(await)内部computation协程完成,它(外部async协程)idles(因此名称为suspend)并将线程返回给线程池,当子computation协程完成时,它(外部async协同程序)唤醒,从池中获取另一个线程并继续?

我提到帖子的原因是https://kotlinlang.org/docs/tutorials/coroutines-basic-jvm.html

  

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

8 个答案:

答案 0 :(得分:21)

暂停功能是所有协程的核心。  暂停功能只是可以在以后暂停和恢复的功能。他们可以执行长时间运行的操作,并等待其完成而不会阻塞。

除了增加了suspend关键字之外,suspending函数的语法与常规函数的语法相似。它可以带有一个参数并具有返回类型。但是,挂起函数只能由另一个挂起函数或在协程中调用。

suspend fun backgroundTask(param: Int): Int {
     // long running operation
}

在后台,挂起函数由编译器转换为不带suspend关键字的另一个函数,该函数采用Continuation类型的附加参数。例如,上面的函数将由编译器转换为:

fun backgroundTask(param: Int, callback: Continuation<Int>): Int {
   // long running operation
}

Continuation是一个接口,其中包含两个函数,这些函数被调用以使用返回值或异常(如果在函数暂停期间发生错误)来恢复协程。

interface Continuation<in T> {
   val context: CoroutineContext
   fun resume(value: T)
   fun resumeWithException(exception: Throwable)
}

答案 1 :(得分:12)

要了解暂停协程的确切含义,我建议您仔细阅读以下代码:

import kotlinx.coroutines.experimental.Unconfined
import kotlinx.coroutines.experimental.launch
import kotlin.coroutines.experimental.Continuation
import kotlin.coroutines.experimental.suspendCoroutine

var continuation: Continuation<Int>? = null

fun main(args: Array<String>) {
    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)...
  6. 请注意,此时,您的效果与launch块中的代码和fun main代码同时执行的效果相同。恰好所有这一切都发生在一个本机线程上,因此launch块被“暂停”。

    现在,在forEach循环代码中,程序会读取continuation函数写入的b()resumes,其值为10resume()的实现方式就像suspendCoroutine调用返回时传入的值一样。所以你突然发现自己正在执行b()。您传递给resume()的值会被分配到i并针对0进行检查。如果它不为零,则while (true)循环在b()内继续,再次到达suspendCoroutine,此时您的resume()调用将返回,现在您将进入另一个循环步骤forEach()。这一直持续到最后你用0继续,然后println语句运行并且程序完成。

    上面的分析应该给你一个重要的直觉,即“暂停协程”意味着将控件返回到最里面的launch调用(或者更常见的是,协程构建器)。如果协程在恢复后再次暂停,则resume()呼叫结束,控制权返回resume()的呼叫者。

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

答案 2 :(得分:4)

  

协程或功能被暂停?

调用暂停 ing 函数将<协程>暂停 s ,这意味着当前线程可以开始执行另一个协程。因此,据说协程被暂停,而不是功能。

但是从技术上讲,此时您的函数将不会由另一个协程执行,因此我们可以说该函数和协程都停止了,但是我们在这里将头发分开。

  

哪些协程被暂停?

外部async启动一个协程。调用computation()时,内部async启动第二个协程。然后,对await()的调用将暂停外部 async协程的执行,直到执行 inner async协程结束了。

您甚至可以看到一个线程:该线程将执行外部async的开始,然后调用computation()并到达内部async。此时,内部异步的主体被跳过,线程继续执行外部async直到到达await()await()是一个“暂停点”,因为await是一个暂停功能。 这意味着外部协程已暂停,因此线程开始执行内部协程。完成后,返回执行外部async的结尾。

  

是否挂起表示在外部异步协程等待(等待)内部计算协程完成时,它(外部异步协程)空闲(因此名称为suspend)并将线程返回到线程池,以及当子级计算协程完成,它(外部异步协程)醒来,从池中获取另一个线程并继续?

是的,

答案 3 :(得分:1)

由于已经有了许多好的答案,我想为其他人提供一个更简单的示例。

  

runBlocking 用例:

  • myMethod()是suspend函数
  • runBlocking { }以阻止方式启动协程。这类似于我们使用Thread类阻塞普通线程并在某些事件之后通知阻塞线程的方式。
  • runBlocking { }阻塞当前执行的线程,直到协程({}之间的主体)完成

     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 { }同时启动协程。
  • 这意味着当我们指定启动时,协程将在worker线程上开始执行。
  • worker线程和外线程(我们称之为launch { })都同时运行。在内部,JVM可能会执行抢占式线程
  • 当我们需要多个任务并行运行时,我们可以使用它。 scopes指定了协程的寿命。如果我们指定GlobalScope,则协程将一直工作到应用程序生命周期结束。

    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会有所帮助。
  • 例如,在下面的代码中,有2个挂起函数myMethod()和myMethod2()。 myMethod2()仅在完全完成myMethod()后才能执行。 OR myMethod2()取决于myMethod()的结果,我们可以使用asyncawait
  • asynclaunch类似地并行启动协程。但是,它提供了一种在并行启动另一个协程之前等待一个协程的方法。
  • 那样就是await()async返回Deffered<T>的实例。默认情况下,TUnit。当我们需要等待任何async完成时,需要在该.await()的{​​{1}}实例上调用Deffered<T>。像下面的示例一样,我们调用了async,这意味着执行将暂停,直到innerAsync.await()完成为止。我们可以在输出中观察到相同的结果。 innerAsync首先完成,然后调用innerAsync。然后下一个myMethod() async开始,它调用innerAsync2

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

答案 4 :(得分:1)

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

启动/ 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)

我认为了解它的一种重要方法是查看编译器对这段代码的处理方式。实际上,它为兰巴创建了一个类。它在类中为“ extra”字符串创建一个属性,然后创建两个函数,一个函数打印“ before”,另一个函数打印“ after”。

有效地,编译器采用了类似于程序代码的代码,并将其转换为回调。

那么“ suspend”关键字有什么作用?它告诉编译器向后寻找生成的回调将需要的上下文。编译器需要知道在哪些“回调”中使用了哪些变量,而suspend关键字对此有所帮助。在此示例中,在挂起之前和之后都使用“额外”变量。因此需要将其拉到包含编译器进行回调的类的适当位置。

它还告诉编译器这是状态的“开始”,并准备将以下代码拆分为回调。 “ startCourtine”仅存在于暂停的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");
            }

答案 5 :(得分:1)

仍然遇到这个问题的任何人,我建议您快速浏览一下。我已经阅读了很多关于这个问题的误导性答案,甚至是一些评分最高的答案。这消除了我的很多疑虑。

https://youtu.be/BOHK_w09pVA?t=577

答案 6 :(得分:0)

我想给你一个关于延续概念的简单例子。这是暂停功能可以冻结/暂停然后继续/继续的功能。停止在线程和信号量方面考虑协程。从连续性甚至回调挂钩的角度考虑它。

需要明确的是,可以通过使用可疑函数来暂停法院。让我们对此进行调查:

在android中,我们可以例如:

var TAG = "myTAG:"
        fun myMethod() {
            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() {
            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

因此您启动的当前功能不会停止,只有协程会在继续运行时挂起。通过运行暂停功能不会暂停线程。

我认为this site can help可以帮助您解决问题,是我的参考。

let做一些很酷的事情,并在迭代过程中冻结我们的暂停功能。我们将稍后在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()
    }

,循环将继续。很高兴知道我们可以随时冻结一个暂停函数,并在一段时间后恢复它。您还可以查看channels

答案 7 :(得分:0)

我发现理解suspend的最好方法是在this关键字和coroutineContext属性之间进行类比。

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

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

问题是:coroutineContext属性 is declared like a "normal" property在Kotlin stdlib中,但是此声明只是用于文档/导航目的的存根。实际上,coroutineContextbuiltin intrinsic property,这意味着在编译器内部魔术师意识到此属性,就像它意识到语言关键字一样。

this关键字对本地函数的作用是coroutineContext属性对suspend函数的作用:它提供对当前执行上下文的访问-在第一种情况下可以访问类实例上下文,并且可以协程实例上下文在第二种情况下。

因此,您需要suspend才能访问coroutineContext属性-当前执行的协程上下文的实例