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


118

我正在阅读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()
    ...
}

says它说“挂起协程直到完成计算”,但是协程就像轻量级线程。因此,如果协程被挂起,如何进行计算?

我们看到await被调用了computation,所以可能是asyncreturn Deferred,这意味着它可以启动另一个协程

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

🤔引用说暂停协程。它是指suspend外部async协程,还是suspend内部computation协程?

是否suspend意味着,虽然外async协程正在等待(await)的内部computation协程来完成,它(外async协程)空闲(故名暂停),并返回线程的线程池,且在儿童computation协同程序完成后,它(外async协程)醒来,从池中获取另一个线程并继续?

我提到该线程的原因是因为https://kotlinlang.org/docs/tutorials/coroutines-basic-jvm.html

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

Answers:


113

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

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

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

在后台,挂起函数由编译器转换为不带suspend关键字的另一个函数,该函数采用type的附加参数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)
}

4
另一个谜揭开了!大!
WindRider

15
我不知道此功能实际上是如何暂停的?他们总是说suspend fun可以停下来,但是究竟是什么呢?
WindRider

2
@WindRider这仅意味着当前线程开始执行其他一些协程,并且稍后将返回该协程。
乔佛里

2
我已经弄清楚了“神秘”机制。可以在“工具”>“ Kotlin”>“字节码”>“反编译btn”的帮助下轻松启动它。它显示了如何通过Continuation等实现所谓的“悬浮点”。任何人都可以自己看看。
WindRider

4
@buzaa这是Roman Elizarov在2017年的演讲,在字节码级别进行了解释。
Marko Topolnik

30

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

import kotlinx.coroutines.Dispatchers.Unconfined
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

var continuation: Continuation<Int>? = null

fun main() = runBlocking {
    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)...

请注意,此时,您将获得与launch块中的fun main代码和您的代码正在同时执行相同的效果。碰巧所有这些都是在单个本机线程上发生的,因此该launch块被“挂起”。

现在,内部forEach循环的代码,程序读取continuationb()功能,写和resumes它的价值10resume()的实现方式就好像suspendCoroutine调用返回的值是您传入的值。因此您突然发现自己处于execute的中间b()。您传递给的值resume()将分配给i并对照进行检查0。如果不为零,则while (true)循环继续进行b(),再次到达suspendCoroutine,此时您的resume()呼叫返回,现在您进入的另一个循环步骤forEach()。一直进行到最后您使用恢复0,然后println语句运行并完成程序。

上面的分析应该给您一个重要的直觉,即“暂停协程”意味着将控件返回到最内层的launch调用(或更普遍地说,是协程构建器)。如果协程在恢复后再次暂停,则resume()调用结束,并且控制权返回给的调用者resume()

协程分派器的存在使这种推理不太清晰,因为它们中的大多数会立即将您的代码提交到另一个线程。在这种情况下,上面的故事发生在另一个线程中,协程分配器也管理该continuation对象,以便在返回值可用时可以恢复该对象。


19

首先,了解此海事组织的最佳资料是Roman Elizarov 的演讲“深入潜入协程”

协程或功能被暂停?

调用暂停荷兰国际集团功能暂停小号的协程,意味着当前线程可以开始执行另一个协程。因此,协程据说是暂停的,而不是功能。

实际上,由于这个原因,挂起功能的呼叫站点称为“挂起点”。

哪个协程被暂停?

让我们看看您的代码并分解发生的情况:

// 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
    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. The control flow 
    //      of the current thread returns to the caller of computation().
    // 3.b. If you have only one thread, the block is sort of "queued" but 
    //      not executed right away, and the control flow returns to the 
    //      caller of computation(). (unless a special dispatcher or 
    //      coroutine start argument is used, but let's keep it simple).
    //    In both cases, we say that this block executes "concurrently"
    //    with C1.
    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)
    }
}

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

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

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

是的,正好。

实际实现此目的的方式是将每个挂起功能转换为状态机,其中每个“状态”对应于此挂起功能内部的挂起点。在后台,该函数可以被调用多次,并提供有关应该从哪个悬浮点开始执行的信息(您应该真正观看我链接的视频以获取有关该信息的更多信息)。


3
很好的答案,我想念关于协程的那种非常基本的解释。
bernardo.g,

为什么不以其他任何语言来实现呢?还是我错过了什么?我一直在考虑该解决方案这么长时间,很高兴Kotlin拥有它,但是不确定TS或Rust为何具有这样的解决方案
PEZO

@PEZO好协程已经存在很长时间了。Kotlin并非发明它们,但是语法和库使它们大放异彩。Go有goroutines,JavaScript和TypeScript有诺言。唯一的区别在于使用它们的语法细节。我发现async以这种方式标记JS的功能很烦人/烦恼,但仍然返回Promise。
乔佛里

抱歉,我的评论不清楚。我指的是suspend关键字。它与异步不同。
PEZO

感谢您指向Roman的视频。纯金。
Denounce'IN

8

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

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

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

问题是:coroutineContext属性 在Kotlin stdlib中像“普通”属性一样被声明,但是此声明只是出于文档/导航目的的存根。实际上coroutineContext内置的内在属性,这意味着在编译器的内部魔术师意识到此属性,就像它意识到语言关键字一样。

哪些this关键字确实为当地的功能是什么coroutineContext属性确实为suspend功能:它可以访问到执行的当前上下文。

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


5

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

为了清楚起见,可以使用suspend函数暂停协程。让我们调查一下:

在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,我们将为它加载协程延续对象:

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

并且循环将继续。很高兴知道我们可以随时冻结一个悬浮函数,并在经过一段时间后恢复它。您也可以调查频道


4

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

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

异步等待用例:

  • 当我们有多个任务做他们依赖于其他的完成,async并且await将帮助。
  • 例如,在下面的代码中,有2暂停函数myMethod()和myMethod2()。myMethod2()只有在myMethod() OR 完全完成后才能执行,具体myMethod2()取决于的结果myMethod(),我们可以使用asyncawait
  • async类似于并行启动一个协程launch。但是,它提供了一种在并行启动另一个协程之前等待一个协程的方法。
  • 那是await()async返回的实例Deffered<T>T将是Unit默认值。当我们需要等待任何对象async的完成时,我们需要调用.await()该对象的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);
            }
        }
    }

输出:

11814-11814/? I/TAG: Outer code started on Thread : main
11814-11814/? I/TAG: Outer code resumed on Thread : main
11814-11845/? D/TAG: Inner code started  on Thread : DefaultDispatcher-worker-2 making outer code suspend
11814-11845/? D/TAG: Inner code i : 1 on Thread : DefaultDispatcher-worker-2
11814-11845/? D/TAG: Inner code i : 2 on Thread : DefaultDispatcher-worker-2
11814-11845/? D/TAG: Inner code i : 3 on Thread : DefaultDispatcher-worker-2
11814-11845/? D/TAG: Inner code i : 4 on Thread : DefaultDispatcher-worker-2
11814-11845/? D/TAG: Inner code i : 5 on Thread : DefaultDispatcher-worker-2
// ---- Due to await() call, innerAsync2 will start only after innerAsync gets completed
11814-11848/? W/TAG: Inner code started  on Thread : DefaultDispatcher-worker-4 making outer code suspend
11814-11848/? W/TAG: Inner code i : 1 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 2 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 3 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 4 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 5 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 6 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 7 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 8 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 9 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 10 on Thread : DefaultDispatcher-worker-4
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.