我是Kotlin协程的新手,我不知道的一件事是,协程如何知道在拨打网络电话时何时向其他人屈服。
如果我理解正确,协程会抢先工作,这意味着当它有一些耗时的任务(通常是I / O操作)要执行时,它知道何时屈服于其他协程。
例如,假设我们要绘制一些UI,该UI将显示来自远程服务器的数据,并且我们只有一个线程来调度协同程序。我们可以启动一个协程以进行REST API调用以获取数据,而让另一个协程绘制不依赖数据的UI的其余部分。但是,由于我们只有一个线程,因此一次只能运行一个协程。并且除非用于抢先获取数据的协程在等待数据到达时抢先产生,否则这两个协程将按顺序执行。
据我所知,Kotlin的协程实现并没有修补任何现有的JVM实现或JDK网络库。因此,如果协程正在调用REST API,则它应该像使用Java线程一样进行阻塞。我之所以这样说,是因为我在python中似乎有类似的概念,称为绿色线程。为了使其能够与python的内置网络库一起使用,必须首先对网络库进行“ monkey-patch”。对我而言,这是有道理的,因为只有网络库本身才知道何时产生。
那么谁能解释在调用阻塞Java网络API时Kotlin协程如何知道何时产生结果?否则,是否意味着不能同时执行单个线程中的上述示例中提到的任务?
谢谢!
答案 0 :(得分:5)
协程先发制人
不。使用协程,您只能实现协作式多线程,在其中通过显式方法调用挂起和恢复协程。协程仅指出了按需暂停和恢复的问题,而协程调度程序负责确保它在适当的线程上启动和恢复。
学习此代码将帮助您了解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
之后的代码,在这里我们使用延续对象恢复协程。
整个程序在主线程上执行并打印:
main(): launch
Coroutine: started
Coroutine: suspended
main(): resume continuation
Coroutine: resumed
main(): back after resume
我们可以启动一个协程以进行REST API调用以获取数据,而让另一个协程绘制不依赖数据的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库的扩展库。如果您遵循these instructions,写自己的书也非常简单。
答案 1 :(得分:2)
答案是:协程不知道网络调用或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:
有时候可以传递一些局部线程数据很方便,但是对于不与任何特定线程绑定的协程,很难在不编写大量样板的情况下手动实现。
对于ThreadLocal,asContextElement扩展功能在此处用于救援。它创建一个附加的context元素,该元素保留给定ThreadLocal的值,并在协程每次切换其上下文时将其恢复。
很容易在操作中演示它:
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()}'")
}
在此示例中,我们使用Dispatchers.Default在后台线程池中启动新的协程,因此它适用于与线程池不同的线程,但它仍具有我们使用threadLocal指定的线程局部变量的值.asContextElement(value =“ launch”),无论协程执行在哪个线程上。因此,输出(带有调试)为:
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'