首页 > 解决方案 > How does Kotlin coroutines know when to yield when making network calls?

问题描述

I`m new to Kotlin coroutines and one thing I failed to figure out is, how does the coroutines know when to yield to others when making network calls.

If I understand it right, a coroutine works preemptively, which means it knows when to yield to other coroutines when it has some time-consuming tasks(typically I/O operations) to perform.

For example, let`s say we want to paint some UI which will display data from a remote server, and we have only one thread to schedule our coroutines. We could launch one coroutine to make REST API calls to get the data, while having another coroutine paint the rest of the UI which have no dependency on the data. However, since we have only one thread, there could only be one coroutine running at a time. And unless the coroutine which is used to fetch data preemptively yields while it is waiting for data to arrive, the two coroutines would be executed sequentially.

As far as I know, Kotlin's coroutine implementation does not patch any of existing JVM implementation or JDK network libraries. So if a coroutine is calling a REST API, it should block just like this is done using a Java thread. I'm saying this because I've seem similar concepts in python which are called green threads. And in order for it to work with python`s built-in network library, one must 'monkey-patch' the network library first. And to me this makes sense because only the network library itself knows when to yield.

So could anyone explain how Kotlin coroutine knows when to yield when calling blocking Java network APIs? Or if it does not, then does it mean the tasks mentioned in the example above could not be performed concurrently give a single thread?

Thanks!

标签: javakotlincoroutinekotlin-coroutines

解决方案


协程抢先工作

没有。使用协程,您只能实现协作多线程,在这种情况下,您可以通过显式方法调用来暂停和恢复协程。协程只关注按需挂起和恢复,而协程调度程序负责确保它在适当的线程上启动和恢复。

研究这段代码将帮助你了解 Kotlin 协程的精髓:

import kotlinx.coroutines.experimental.*
import kotlin.coroutines.experimental.*

fun main(args: Array<String>) {
    var continuation: Continuation<Unit>? = null
    println("main(): launch")
    GlobalScope.launch(Dispatchers.Unconfined) {
        println("Coroutine: started")
        suspendCoroutine<Unit> {
            println("Coroutine: suspended")
            continuation = it
        }
        println("Coroutine: resumed")
    }
    println("main(): resume continuation")
    continuation!!.resume(Unit)
    println("main(): back after resume")
}

这里我们使用最简单的Unconfined调度器,它不做任何调度,它在你调用launch { ... }和的地方运行协程continuation.resume()。协程通过调用suspendCoroutine. 这个函数通过传递你以后可以用来恢复协程的对象来运行你提供的块。我们的代码将其保存到var continuation. 控制权返回到 之后的代码launch,我们使用 continuation 对象来恢复协程。

整个程序在主线程上执行并打印:

main(): launch
Coroutine: started
Coroutine: suspended
main(): resume continuation
Coroutine: resumed
main(): back after resume

我们可以启动一个协程来调用 REST API 来获取数据,同时让另一个协程绘制 UI 的其余部分,这些 UI 不依赖于数据。

这实际上描述了您将使用普通线程做什么。协程的优点是您可以在 GUI 绑定代码的中间进行“阻塞”调用,它不会冻结 GUI。在您的示例中,您将编写一个进行网络调用然后更新 GUI 的协程。当网络请求正在进行时,协程被挂起,其他事件处理程序运行,使 GUI 保持活动状态。处理程序不是协程,它们只是常规的 GUI 回调。

用最简单的话来说,你可以编写这个 Android 代码:

activity.launch(Dispatchers.Main) {
    textView.text = requestStringFromNetwork()
}

...

suspend fun requestStringFromNetwork() = suspendCancellableCoroutine<String> {
    ...
}

requestStringFromNetwork相当于“修补 IO 层”,但您实际上并没有修补任何东西,您只是围绕 IO 库的公共 API 编写包装器。几乎所有 Kotlin IO 库都添加了这些包装器,并且还有 Java IO 库的扩展库。如果您按照这些说明编写自己的内容也非常简单。


推荐阅读