前言
上一篇文章中,我们使用launch
启动了一个用来做一劳永逸任务的协程,我们把它看成射箭,从代码调用地方射出去,射出去的箭该干啥就干啥,协程后面代码继续执行,即体现出非阻塞特性。
正文
本篇文章我们来看其他2个启动协程的API:runBlocking
和async
。
runBlocking
其实runBlocking
从名字就能看的出来,是阻塞的意思,所以它启动的协程特点就是会阻塞线程执行。
这里其实可以从两个方面来理解:
- 一个是阻塞线程,比如Android程序在主线程调用
runBlocking
函数,这时线程将进入等待状态,等待runBlocking函数执行完成,假如该函数执行太久,就会导致ANR。 - 另一个方式是,在
runBlocking
启动的协程中,我们使用launch
再启动子协程,这时runBlocking
会等待子协程执行完成。
这里的理解难点是"阻塞线程",所以runBlocking
只用于推荐连接线程和协程。
我们和launch
对比一下,比如都在Android主线程启动协程,runBlocking
会全占线程,Android的绘制UI工作将无法执行,而使用launch
时,Android的绘制UI工作可以正常执行。
我们来看个简单例子:
fun main() {
runBlocking {
println("coroutine start")
//模拟耗时
delay(1000)
println("coroutine end")
}
println("process end")
}
这段代码的执行结果如下:
在这里我们发现,runBlocking
启动的协程和launch
就有非常大的区别了,在调用runBlocking
那一刻开始,程序的执行就进入了协程内部,等协程内部执行完后,才继续执行线程上的代码。
这里或许注意到一点,之前launch
启动时,必须要有协程范围,而runBlocking
却不需要,我们看一下其方法定义:
public actual fun <T> runBlocking(context: CoroutineContext,
block: suspend CoroutineScope.() -> T): T {
...
}
会发现它不是CoroutineScope
的扩展函数,同时block
的函数类型返回值是T,说明是可以返回值的,我们这里来尝试一下:
fun main() {
val result =runBlocking {
println("coroutine start")
//模拟耗时
delay(1000)
println("coroutine end")
return@runBlocking "hello"
}
println("process end $result")
}
这里的代码结果如下:
可以发现result
是有结果的。
那有没有即不阻塞线程,又可以获得协程结果的方法呢 那就是async
了。
async
async
方法集成了前面2种的优点,话不多说,直接看下面代码:
fun main() = runBlocking {
println("coroutine start ${Thread.currentThread().name}")
val result = async {
println("inner coroutine start ${Thread.currentThread().name}")
//模拟耗时
delay(1000)
println("inner coroutine end ${Thread.currentThread().name}")
return@async "fine!"
}
println("coroutine continue")
println("coroutine end ${result.await()}")
}
首先这里会发现之前launch
都是使用GlobalScope
,但是Kotlin官方不建议使用,所以这里我们使用runBlocking
来启动一个协程(虽然runBlocking
不建议在项目中使用,但是测试demo可以使用)。
根据前面所说的runBlocking
特点,在它里面启动的协程中,再启动其他的协程,会等待子协程执行完,所以这里我们就免去了使用Thread.sleep
的写法了。
其次为什么在这里面可以用async
启动协程呢?我们可以看一下前面runBlocking
的参数block
的参数类型:block: suspend CoroutineScope.() -> T
,它是一个带接收者的函数类型。根据带接收者函数类型的定义,该block
就相当于是CoroutineScope
的成员方法了。
所以在lambda中已经有了CoroutineScope
,所以可以调用async
方法,下面是上面代码的执行结果:
会发现协程1开始后,接着就是调用continue
,说明async
没有阻塞后续代码执行,然后是协程2开始运行,当调用await()
方法时,能获取到结果。
关于await()
方法,后面挂起函数文章中会具体说明其实现原理。
我们来看一下async函数的定义:
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
...
}
从这里我们可以看出,block
类型是有返回值T的,而不是像launch
返回Unit
,同时async
返回的是Deferred<T>
,可以通过这个拿到协程返回值。
钓鱼模型
既然之前launch
可以类比与射箭,那async
用钓鱼来做比喻就再恰当不过了。
其中Deferred
就相当于是鱼竿,我们把鱼饵和鱼钩甩入水中,就相当于协程已经开始工作了,这时主线程可以继续干别的事。
当需要返回结果时,再拿起鱼竿,得到钓的鱼(返回结果)。注意,拿起鱼竿这个事是阻塞后续代码执行的,假如调用await
方法时,async
的结果还没有,这时是需要等待运行结束的,这是非常符合逻辑的。
我们最常见的可以使用async
来同时开启多个请求,等请求结果都拿到,再接着进行后续操作。
这个模型可以完美解释非阻塞和拿到返回结果,同时还有一些细节,比如下面代码:
fun main() = runBlocking {
val deferred: Deferred<String> = async {
println("In async:${Thread.currentThread().name}")
delay(1000L) // 模拟耗时操作
println("In async after delay!")
return@async "Task completed!"
}
// 不再调用 deferred.await()
delay(2000L)
}
这里我们不再调用await
方法,结果如下:
会发现启动的协程依旧执行了,这就相当于我钓鱼把鱼钩丢入水中,协程已经运行了,但是我没有抬杆,这个await
函数就相当于是抬杆,没有获取结果而已。
阻塞探究
这里说async
不完全阻塞也是不对的,原因很简单,当你调用await()
方法时,会等待返回结果,也是不会继续方法剩余代码的,但是注意它不会阻塞线程的,就比如你的方法在Android主线程调用async
,线程依旧可以绘制UI,只是方法暂时会被挂起不执行,等待一个合适的时机继续执行。
这个思考点很有意思,重点是理解挂起
,await()
挂起函数到底挂起了什么?它挂起了它后面的代码,可以等恢复后再继续执行。但是Android主线程该干啥还是干啥,不会阻塞线程。
但是它有个很关键的用法,就是我可以同时async
多个协程,然后这些协程并发执行耗时操作,这样可以解决串行增加耗时的问题。
所以这里的阻塞可以理解为挂起
,只是将这个方法的执行给暂停(挂起)了,等一会获取到结果再继续执行。
下面有个gif图,可以增强理解一下协程代码执行的顺序:
这个gif图有一个容易理解有误的点,就是认为它会阻塞线程。这里阻塞线程的原因是,我们使用了runBlocking
,假如上面代码运行在launch
启动的协程中,就完全好理解了。
总结
runBlocking
的主要作用是连接协程和线程,我们要少用。一方面是会阻塞线程执行,另一方面是它会等待它的子协程执行完成。
async
创建的协程就像是钓鱼,把鱼竿抛出去我们继续干活。await
是获取协程结果,它是挂起函数,把后面继续执行的操作给挂起来,等获取到结果再继续。