和线程Thread
相比,Kotlin的Coroutines非常的轻量。
开启一个新的Coroutines可以使用launch
,async
或者runBlocking
三个中的一个。不同的第三方块库也会定义其他的启动方法。
async
async
启动新的Coroutine会返回一个Deferred
对象。Deferred
和线程池中的Future
很像,这个对象里可以包含计算的返回值。launch
launch
和async
最大的不同点在于launch
被用于不关注执行返回结果的计算,概念上和线程池中的Runnable
很像。但是launch
会返回一个Job
对象,这个Job
对象表示当前的coroutine,可以用于控制当前计算的生命周期。调用的Job.join()
方法可能会阻塞直到launch
中的计算完成。
实际上之前提到的Deferred
是继承Job
的泛型类,区别在于Deferred
中的泛型就是是执行结果类型。可以通过调用Deferred.await()
方法获取计算结果,同样的也可控制生命周期。runBlocking
runBlocking
作为普通方法和suspend
方法互相调用的桥梁,实现程序在阻塞和非阻塞状态之间切换。通常在main
方法和测试代码中用作启动一个顶层的主coroutine。
概念还是比较抽象,看下面这段代码可以帮助理解runBlocking
的桥梁作用:
fun main() = runBlocking {
val deferred: Deferred<Int> = async {
loadData()
}
log("waiting...")
println(deferred.await())
}
suspend fun loadData(): Int {
log("loading...")
delay(1000L)
log("loaded!")
return 42
}
代码很简单,loadData()
模拟了网络请求,但是这里面还有一些东西值得我们具体分析下。首先是上面这段代码是否是非阻塞的?有可能觉得这不废话嘛,用了Coroutine当然是非阻塞的。实际情况在跑一下看下log:
354 [main] INFO Contributors - waiting...
370 [main] INFO Contributors - loading...
1376 [main] INFO Contributors - loaded!
42
注意看[main]
这里打印的是当前线程,可以看到所有的代码都是main线程执行的。在给出解决方案之前,先分析下这段代码的执行流程。首先是通过runBlocking{}
启动了一个coroutine,这个coroutine是在main线程启动的。然后又通过async{}
启动了另一个coroutine,那么这个coroutine是在哪里启动的呢?还是main线程,因为没有明确指定启动线程的情况下coroutine会在外部启动他的coroutine scope中运行(就是runBlocking{}
)。而一个线程同时只能运行一个coroutine,那么在runBlocking{}
启动的coroutine就会进入suspended状态后,启动了async{}
启动的coroutine。可以用下面这张图总结下运行状态:
很明显,所有的代码都是在main线程执行的。那么如何让
loadData()
在其他线程执行呢?不难想到只要让async{}
在其他线程启动就行了。Coroutine是通过指定Dispatcher
的方法来指定coroutine运行线程的。代码如下:
fun main() = runBlocking {
val deferred: Deferred<Int> = async(Dispatchers.Default) {//<-注意看这里的Dispatcher
loadData()
}
log("waiting...")
println(deferred.await())
log("finish...")
}
suspend fun loadData(): Int {
log("loading...")
delay(1000L)
log("loaded!")
return 42
}
指定了async{}
由Dispatchers.Default
来启动,再来看下log:
176 [main] INFO Contributors - waiting...
176 [DefaultDispatcher-worker-1] INFO Contributors - loading...
1184 [DefaultDispatcher-worker-1] INFO Contributors - loaded!
42
1186 [main] INFO Contributors - finish...
很明显,看到开始和结束都是在main线程中,而模拟加载数据的loadData()
运行在DefaultDispatcher-worker-1线程中,这就很符合我们对Coroutine的使用期望了。那么,这个Dispatchers.Default
是什么来头?从注释
The default CoroutineDispatcher that is used by all standard builders like launch, async, etc if no dispatcher nor any other ContinuationInterceptor is specified in their context.
It is backed by a shared pool of threads on JVM. By default, the maximal level of parallelism used by this dispatcher is equal to the number of CPU cores, but is at least two. Level of parallelism X guarantees that no more than X tasks can be executed in this dispatcher in parallel.
可以发现这就是一个由CPU核心个数线程构成的线程池(如果是单核的话,最少是两个线程)。所以Kotlin中的Coroutine底层依然是线程池,可以说是太阳底下没有新鲜事。那么用一张图总结下这次的执行流程:
还有一个问题,如果是要在loadData()
中更新主线程UI怎么办?见下面代码:
fun main() = runBlocking {
val deferred: Deferred<Int> = async(Dispatchers.Default) {
loadData()
}
log("waiting...")
println(deferred.await())
log("finish...")
}
suspend fun loadData(): Int {
log("loading...")
withContext(Dispatchers.Main){//<---回到主线程
//update ui here
log("updating main...")
}
delay(1000L)
log("loaded!")
return 42
}
执行到 withContext(Dispatchers.Main){}
会suspend当前的coroutine直到完成 withContext(Dispatchers.Main){}
这里的代码。当然也可以使用launch(context) { ... }.join()
来完成相同的功能。