前言
在上一篇文章中,我们介绍了如何使用launch函数来启动一个协程,它会在后台运行,不会阻塞主线程。我们用射箭的比喻来形象地解释了协程的特点,即射出去的箭该干啥就干啥,不会等待结果返回。例如:
// 射出一支箭
launch {
println("Arrow 1 is flying...")
delay(1000) // 模拟箭飞行的时间
println("Arrow 1 hits the target!")
}
// 射出第二支箭
launch {
println("Arrow 2 is flying...")
delay(2000) // 模拟箭飞行的时间
println("Arrow 2 hits the target!")
}
// 射出第三支箭
launch {
println("Arrow 3 is flying...")
delay(3000) // 模拟箭飞行的时间
println("Arrow 3 hits the target!")
}
println("All arrows are shot!")
输出结果如下:
All arrows are shot!
Arrow 1 is flying...
Arrow 2 is flying...
Arrow 3 is flying...
Arrow 1 hits the target!
Arrow 2 hits the target!
Arrow 3 hits the target!
可以看到,主线程在射出所有的箭后就结束了,不会等待任何一个箭的结果。而每个箭都是一个协程,它们在后台并发地执行,按照自己的速度完成任务。
但是有时候,我们可能需要获取协程的结果,或者等待协程完成后再执行其他操作。这时候,我们就需要用到另外两种启动协程的API:runBlocking和async。
runBlocking
runBlocking函数可以创建一个阻塞当前线程的协程,并等待它及其所有子协程完成后再恢复线程执行。它可以用来连接协程和线程之间的桥梁,让我们可以在普通函数中调用协程函数。例如:
fun main() {
// 在主线程中启动一个阻塞协程
runBlocking {
println("runBlocking start")
// 在阻塞协程中启动一个子协程
launch {
println("launch start")
delay(1000) // 模拟耗时操作
println("launch end")
}
println("runBlocking end")
}
println("main end")
}
输出结果如下:
runBlocking start
launch start
runBlocking end
launch end
main end
可以看到,主线程在进入runBlocking后被阻塞了,直到runBlocking内部的子协程完成后才继续执行。而子协程仍然是非阻塞的,它会在后台运行,并在延迟后结束。
另外,runBlocking还可以返回一个值,这个值就是它内部最后一行代码的结果。例如:
fun main() {
// 在主线程中启动一个阻塞协程,并获取它的返回值
val result = runBlocking {
println("runBlocking start")
// 在阻塞协程中启动一个子协程,并获取它的返回值
val result = launch {
println("launch start")
delay(1000) // 模拟耗时操作
println("launch end")
"Hello" // 返回一个字符串值
}
println("runBlocking end")
result // 返回子协程的返回值
}
println("main end, result is $result")
}
输出结果如下:
runBlocking start
launch start
runBlocking end
launch end
main end, result is Hello
可以看到,主线程在获取到runBlocking的返回值后才结束,而这个返回值就是子协程的返回值。
async
async函数可以创建一个可以返回结果的协程,它不会阻塞当前线程,但会挂起后续代码直到调用await方法获取结果。它可以用来实现协程之间的依赖关系,让我们可以在一个协程中等待另一个协程的结果。例如:
fun main() {
// 在主线程中启动一个协程
GlobalScope.launch {
println("launch start")
// 在协程中启动一个可以返回结果的协程,并获取它的返回值
val result = async {
println("async start")
delay(1000) // 模拟耗时操作
println("async end")
"World" // 返回一个字符串值
}.await() // 调用await方法获取结果,会挂起当前协程直到结果返回
println("launch end, result is $result")
}
println("main end")
}
输出结果如下:
main end
launch start
async start
async end
launch end, result is World
可以看到,主线程在启动协程后就结束了,不会等待任何结果。而协程在启动async后会挂起,直到async返回结果后才继续执行。
另外,async还可以同时启动多个协程,并发地执行它们,然后等待所有的结果返回。这样可以提高效率,避免串行地等待每个协程。例如:
fun main() {
// 在主线程中启动一个协程
GlobalScope.launch {
println("launch start")
// 在协程中同时启动三个可以返回结果的协程,并分别获取它们的返回值
val result1 = async {
println("async 1 start")
delay(1000) // 模拟耗时操作
println("async 1 end")
"Hello" // 返回一个字符串值
}
val result2 = async {
println("async 2 start")
delay(2000) // 模拟耗时操作
println("async 2 end")
"World" // 返回一个字符串值
}
val result3 = async {
println("async 3 start")
delay(3000) // 模拟耗时操作
println("async 3 end")
"!" // 返回一个字符串值
}
// 调用await方法获取结果,会挂起当前协程直到所有结果返回
val finalResult = result1.await() + " " + result2.await() + result3.await()
println("launch end, final result is $finalResult")
}
println("main end")
}
输出结果如下:
main end
launch start
async 1 start
async 2 start
async 3 start
async 1 end
async 2 end
async 3 end
launch end, final result is Hello World!
可以看到,三个async都是并发地执行的,它们的执行时间取决于各自的延迟时间。而协程在等待所有的结果返回后才继续执行。
钓鱼模型
为了更形象地理解async启动的协程,我们可以用钓鱼的比喻来解释它们。我们可以把每个Deferred对象(即async返回的对象)看作是一根鱼竿,它有一个await方法来抬杆收鱼。而每个鱼竿都有一个钓鱼时间,就是它内部代码执行的时间。例如:
fun main() {
// 在主线程中启动一个协程
GlobalScope.launch {
println("开始钓鱼")
// 在协程中同时启动三个可以返回结果的协程,并分别获取它们的返回值
val rod1 = async {
println("鱼竿1下水")
delay(1000) // 模拟钓鱼时间
println("鱼竿1上钩")
"鲫鱼" // 返回一个字符串值
}
val rod2 = async {
println("鱼竿2下水")
delay(2000) // 模拟钓鱼时间
println("鱼竿2上钩")
"草鱼" // 返回一个字符串值
}
val rod3 = async {
println("鱼竿3下水")
delay(3000) // 模拟钓鱼时间
println("鱼竿3上钩")
"鲤鱼" // 返回一个字符串值
}
// 调用await方法获取结果,会挂起当前协程直到所有结果返回
val fish1 = rod1.await() // 抬杆收鲫鱼
val fish2 = rod2.await() // 抬杆收草鱼
val fish3 = rod3.await() // 抬杆收鲤鱼
println("结束钓鱼,收获了$fish1, $fish2, $fish3")
}
println("主线程结束")
}
输出结果如下:
主线程结束
开始钓鱼
鱼竿1下水
鱼竿2下水
鱼竿3下水
鱼竿1上钩
鱼竿2上钩
鱼竿3上钩
结束钓鱼,收获了鲫鱼, 草鱼, 鲤鱼
可以看到,三个async都是并发地执行的,它们的执行时间取决于各自的延迟时间。而协程在等待所有的结果返回后才继续执行。
阻塞探究
我们已经知道了await方法会挂起当前协程,直到获取结果为止。但是这里的挂起并不是阻塞,它只是暂停了当前协程的执行,而不是阻塞了当前线程。这意味着当前线程可以继续做其他事情,而不是等待协程的结果。例如:
fun main() {
// 在主线程中启动一个协程
GlobalScope.launch {
println("launch start")
// 在协程中同时启动两个可以返回结果的协程,并分别获取它们的返回值
val result1 = async {
println("async 1 start")
delay(1000) // 模拟耗时操作
println("async 1 end")
"Hello" // 返回一个字符串值
}
val result2 = async {
println("async 2 start")
delay(2000) // 模拟耗时操作
println("async 2 end")
"World" // 返回一个字符串值
}
// 调用await方法获取结果,会挂起当前协程直到结果返回
val finalResult = result1.await() + " " + result2.await()
println("launch end, final result is $finalResult")
}
// 在主线程中循环打印点号
repeat(10) {
print(".")
Thread.sleep(500) // 模拟耗时操作
}
println()
println("main end")
}
输出结果如下:
main end
launch start
async 1 start
async 2 start
....async 1 end
....async 2 end
launch end, final result is Hello World
..
main end
可以看到,主线程在启动协程后并没有被阻塞,而是继续循环打印点号。而协程在等待两个async的结果时也没有阻塞线程,而是让出了线程资源给主线程。当两个async都返回结果后,协程才恢复执行,并打印出最终结果。
总结
runBlocking和async都是启动协程的API,它们有以下优缺点和适用场景:
-
runBlocking可以创建一个阻塞当前线程的协程,并等待它及其所有子协程完成后再恢复线程执行。它可以用来连接协程和线程之间的桥梁,让我们可以在普通函数中调用协程函数。它还可以返回一个值,这个值就是它内部最后一行代码的结果。
-
runBlocking的缺点是它会阻塞当前线程,导致线程资源浪费。它不适合在高并发的场景中使用,因为它会降低系统的吞吐量和响应速度。
-
async可以创建一个可以返回结果的协程,它不会阻塞当前线程,但会挂起后续代码直到调用await方法获取结果。它可以用来实现协程之间的依赖关系,让我们可以在一个协程中等待另一个协程的结果。它还可以同时启动多个协程,并发地执行它们,然后等待所有的结果返回。这样可以提高效率,避免串行地等待每个协程。
-
async的缺点是它会增加代码的复杂度,因为它需要我们手动管理每个协程的返回值和异常。它还需要我们注意协程的作用域和生命周期,避免内存泄漏和僵尸协程的产生。
-
async适合在需要获取协程结果或者需要并发执行多个协程的场景中使用,因为它可以提高代码的可读性和性能。