在进行网络调用时,Kotlin 协程如何知道何时让步?
How does Kotlin coroutines know when to yield when making network calls?
我是 Kotlin 协程的新手,我未能弄清楚的一件事是,协程如何知道在进行网络调用时何时让步给其他程序。
如果我理解正确的话,一个协程会抢先工作,这意味着当它有一些耗时的任务(通常是 I/O 操作)要执行时,它知道什么时候让步给其他协程。
例如,假设我们想要绘制一些 UI 来显示来自远程服务器的数据,并且我们只有一个线程来调度协程。我们可以启动一个协程来进行 REST API 调用以获取数据,同时让另一个协程绘制 UI 的其余部分,这些 UI 不依赖于数据。但是,由于我们只有一个线程,因此一次只能有一个协程运行。除非用于获取数据的协程在等待数据到达时抢先让出,否则这两个协程将顺序执行。
据我所知,Kotlin 的协程实现不会修补任何现有的 JVM 实现或 JDK 网络库。因此,如果协程正在调用 REST API,它应该像使用 Java 线程那样进行阻塞。我这么说是因为我在 python 中看到了类似的概念,它们被称为绿色线程。为了使其与 python 的内置网络库一起使用,必须首先 'monkey-patch' 网络库。对我来说这是有道理的,因为只有网络库本身知道何时放弃。
那么谁能解释一下 Kotlin 协程在调用阻塞 Java 网络 APIs 时如何知道何时让步?或者如果没有,那么是否意味着上面例子中提到的任务不能在单线程中并发执行?
谢谢!
答案是:协程不知道网络调用或I/O操作。你必须根据你的需要编写代码,将繁重的工作封装到不同的协程中,以便它们可以并发执行,因为默认行为是顺序的。
例如:
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // pretend we are doing something useful here (maybe I/O)
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // pretend we are doing something useful here (maybe I/O), too
return 29
}
fun main(args: Array<String>) = runBlocking<Unit> {
val time = measureTimeMillis {
val one = doSomethingUsefulOne()
val two = doSomethingUsefulTwo()
println("The answer is ${one + two}")
}
println("Completed in $time ms")
}
会产生这样的结果:
The answer is 42
Completed in 2017 ms
并且doSomethingUsefulOne()和doSomethingUsefulTwo()会依次执行。
如果你想并发执行,你必须写成:
fun main(args: Array<String>) = runBlocking<Unit> {
val time = measureTimeMillis {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
}
将产生:
The answer is 42
Completed in 1017 ms
因为 doSomethingUsefulOne() 和 doSomethingUsefulTwo() 将同时执行。
更新:
关于协同程序的执行位置,我们可以在 github 项目指南中阅读 https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md#thread-local-data:
Sometimes it is convenient to have an ability to pass some thread-local data, but, for coroutines, which are not bound to any particular thread, it is hard to achieve it manually without writing a lot of boilerplate.
For ThreadLocal, asContextElement extension function is here for the rescue. It creates an additional context element, which keep the value of the given ThreadLocal and restores it every time the coroutine switches its context.
It is easy to demonstrate it in action:
val threadLocal = ThreadLocal<String?>() // declare thread-local variable
fun main(args: Array<String>) = runBlocking<Unit> {
threadLocal.set("main")
println("Pre-main, current thread: ${Thread.currentThread()}, threadlocal value: '${threadLocal.get()}'")
val job = launch(Dispatchers.Default + threadLocal.asContextElement(value = "launch")) {
println("Launch start, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
yield()
println("After yield, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
}
job.join()
println("Post-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
}
In this example we launch new coroutine in a background thread pool using Dispatchers.Default, so it works on a different threads from a thread pool, but it still has the value of thread local variable, that we've specified using threadLocal.asContextElement(value = "launch"), no matter on what thread the coroutine is executed. Thus, output (with debug) is:
Pre-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'
Launch start, current thread: Thread[CommonPool-worker-1 @coroutine#2,5,main], thread local value: 'launch'
After yield, current thread: Thread[CommonPool-worker-2 @coroutine#2,5,main], thread local value: 'launch'
Post-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'
a coroutine works preemptively
没有。使用协同程序,您只能实现协作式多线程,您可以通过显式方法调用暂停和恢复协同程序。协程只关注按需暂停和恢复,而 协程调度程序 负责确保它在适当的线程上启动和恢复。
学习这段代码将帮助您了解 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 { ... }
和 [=15= 的协程].协程通过调用 suspendCoroutine
挂起自己。此函数 运行 通过将对象传递给您提供的块,您稍后可以使用该对象来恢复协程。我们的代码将它保存到var continuation
。控制returns到launch
之后的代码,这里我们使用continuation对象恢复协程
整个程序在主线程上执行并打印:
main(): launch
Coroutine: started
Coroutine: suspended
main(): resume continuation
Coroutine: resumed
main(): back after resume
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.
这实际上描述了您将使用普通线程执行的操作。协程的优点是您可以在 GUI 绑定代码中间进行 "blocking" 调用,它不会冻结 GUI。在您的示例中,您将编写一个协程来进行网络调用,然后更新 GUI。当网络请求正在进行时,协程被挂起,其他事件处理程序 运行,保持 GUI 活动。处理程序不是协程,它们只是常规的 GUI 回调。
用最简单的话来说,你可以写出这个Android代码:
activity.launch(Dispatchers.Main) {
textView.text = requestStringFromNetwork()
}
...
suspend fun requestStringFromNetwork() = suspendCancellableCoroutine<String> {
...
}
requestStringFromNetwork
等同于 "patching the IO layer",但你实际上并没有修补任何东西,你只是在 IO 库的 public API 周围编写包装器。几乎所有 Kotlin IO 库都添加了这些包装器,并且还有 Java IO 库的扩展库。按照these instructions.
自己写也很直接
我是 Kotlin 协程的新手,我未能弄清楚的一件事是,协程如何知道在进行网络调用时何时让步给其他程序。
如果我理解正确的话,一个协程会抢先工作,这意味着当它有一些耗时的任务(通常是 I/O 操作)要执行时,它知道什么时候让步给其他协程。
例如,假设我们想要绘制一些 UI 来显示来自远程服务器的数据,并且我们只有一个线程来调度协程。我们可以启动一个协程来进行 REST API 调用以获取数据,同时让另一个协程绘制 UI 的其余部分,这些 UI 不依赖于数据。但是,由于我们只有一个线程,因此一次只能有一个协程运行。除非用于获取数据的协程在等待数据到达时抢先让出,否则这两个协程将顺序执行。
据我所知,Kotlin 的协程实现不会修补任何现有的 JVM 实现或 JDK 网络库。因此,如果协程正在调用 REST API,它应该像使用 Java 线程那样进行阻塞。我这么说是因为我在 python 中看到了类似的概念,它们被称为绿色线程。为了使其与 python 的内置网络库一起使用,必须首先 'monkey-patch' 网络库。对我来说这是有道理的,因为只有网络库本身知道何时放弃。
那么谁能解释一下 Kotlin 协程在调用阻塞 Java 网络 APIs 时如何知道何时让步?或者如果没有,那么是否意味着上面例子中提到的任务不能在单线程中并发执行?
谢谢!
答案是:协程不知道网络调用或I/O操作。你必须根据你的需要编写代码,将繁重的工作封装到不同的协程中,以便它们可以并发执行,因为默认行为是顺序的。
例如:
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // pretend we are doing something useful here (maybe I/O)
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // pretend we are doing something useful here (maybe I/O), too
return 29
}
fun main(args: Array<String>) = runBlocking<Unit> {
val time = measureTimeMillis {
val one = doSomethingUsefulOne()
val two = doSomethingUsefulTwo()
println("The answer is ${one + two}")
}
println("Completed in $time ms")
}
会产生这样的结果:
The answer is 42
Completed in 2017 ms
并且doSomethingUsefulOne()和doSomethingUsefulTwo()会依次执行。 如果你想并发执行,你必须写成:
fun main(args: Array<String>) = runBlocking<Unit> {
val time = measureTimeMillis {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
}
将产生:
The answer is 42
Completed in 1017 ms
因为 doSomethingUsefulOne() 和 doSomethingUsefulTwo() 将同时执行。
更新: 关于协同程序的执行位置,我们可以在 github 项目指南中阅读 https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md#thread-local-data:
Sometimes it is convenient to have an ability to pass some thread-local data, but, for coroutines, which are not bound to any particular thread, it is hard to achieve it manually without writing a lot of boilerplate.
For ThreadLocal, asContextElement extension function is here for the rescue. It creates an additional context element, which keep the value of the given ThreadLocal and restores it every time the coroutine switches its context.
It is easy to demonstrate it in action:
val threadLocal = ThreadLocal<String?>() // declare thread-local variable
fun main(args: Array<String>) = runBlocking<Unit> {
threadLocal.set("main")
println("Pre-main, current thread: ${Thread.currentThread()}, threadlocal value: '${threadLocal.get()}'")
val job = launch(Dispatchers.Default + threadLocal.asContextElement(value = "launch")) {
println("Launch start, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
yield()
println("After yield, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
}
job.join()
println("Post-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
}
In this example we launch new coroutine in a background thread pool using Dispatchers.Default, so it works on a different threads from a thread pool, but it still has the value of thread local variable, that we've specified using threadLocal.asContextElement(value = "launch"), no matter on what thread the coroutine is executed. Thus, output (with debug) is:
Pre-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'
Launch start, current thread: Thread[CommonPool-worker-1 @coroutine#2,5,main], thread local value: 'launch'
After yield, current thread: Thread[CommonPool-worker-2 @coroutine#2,5,main], thread local value: 'launch'
Post-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'
a coroutine works preemptively
没有。使用协同程序,您只能实现协作式多线程,您可以通过显式方法调用暂停和恢复协同程序。协程只关注按需暂停和恢复,而 协程调度程序 负责确保它在适当的线程上启动和恢复。
学习这段代码将帮助您了解 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 { ... }
和 [=15= 的协程].协程通过调用 suspendCoroutine
挂起自己。此函数 运行 通过将对象传递给您提供的块,您稍后可以使用该对象来恢复协程。我们的代码将它保存到var continuation
。控制returns到launch
之后的代码,这里我们使用continuation对象恢复协程
整个程序在主线程上执行并打印:
main(): launch
Coroutine: started
Coroutine: suspended
main(): resume continuation
Coroutine: resumed
main(): back after resume
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.
这实际上描述了您将使用普通线程执行的操作。协程的优点是您可以在 GUI 绑定代码中间进行 "blocking" 调用,它不会冻结 GUI。在您的示例中,您将编写一个协程来进行网络调用,然后更新 GUI。当网络请求正在进行时,协程被挂起,其他事件处理程序 运行,保持 GUI 活动。处理程序不是协程,它们只是常规的 GUI 回调。
用最简单的话来说,你可以写出这个Android代码:
activity.launch(Dispatchers.Main) {
textView.text = requestStringFromNetwork()
}
...
suspend fun requestStringFromNetwork() = suspendCancellableCoroutine<String> {
...
}
requestStringFromNetwork
等同于 "patching the IO layer",但你实际上并没有修补任何东西,你只是在 IO 库的 public API 周围编写包装器。几乎所有 Kotlin IO 库都添加了这些包装器,并且还有 Java IO 库的扩展库。按照these instructions.