执行编排指的是对异步函数的执行顺序进行控制,举个简单的例子:
需要调用a、b两个函数,从高效的角度讲a、b并行调用有一定是最佳的,但是如果b函数的输入依赖a函数的输出,那就只能做串行调用。
针对a、b两个函数执行顺序的控制,即本节要讲的执行编排
,原文中的 Composing Suspending Functions
默认串行执行
假设我们有两个suspending的函数,可能是远程调用(rpc)。我们假装这两个函数很有用,虽然这两个函数只是简单的delay
了一下.
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // pretend we are doing something useful here
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // pretend we are doing something useful here, too
return 29
}
我们如何让这两个函数串行的执行呢?依次执行doSomethingUsefulOne和doSomethingUsefulTwo,然后计算两个函数结果的和,请看下面的代码(为了计算两个函数总的执行时间,此处利用了measureTimeMillis
函数)
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
并行执行异步操作
如果两次函数调用之间没有有起来关系,我们就可以并行的执行两个函数了,这样执行起来总的耗时会减少。
从概念上讲,async和launch是相似的。他们都可以启动一个协程,区别在于launch返回一个job对象,job对象不携带任何结果相关的信息。而async返回一个Deferred类型的对象,和future类似,不过这个是在协程背景下的,可以通过.await()
函数来获得结果。而且Deferred也是一个job,一样可以用来cancel协程。
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
执行速度有两倍的提升,因为两个协程是并行执行的。谨记:利用协程的并行永远是显式的。
延迟启动异步动作
作为一个可选项,在使用async关键字启动协程的时候,可以传入参数CoroutineStart.LAZY
。在这种模式下,协程启动的时机:
- await函数被调用时
- start函数被调用时
val time = measureTimeMillis {
val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
// some computation
one.start() // start the first one
two.start() // start the second one
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
结果如下:
The answer is 42
Completed in 1017 ms
上面的例子中,两个协程并没有像之前的例子一样立刻启动,启动的控制权交给了编码者,编码者可以通过start函数或者await函数进行启动。
但是需要注意的是,如果使用await来触发协程的启动,上面的例子会变为串行执行,因为aswit函数会等待协程执行完毕才向下执行接下来的动作。
异步风格的函数
我们可以通过 async coroutine builder
和显式的 GlobalScope
来把 doSomethingUsefulOne
函数和doSomethingUsefulTwo
函数声名为异步函数:
// The result type of somethingUsefulOneAsync is Deferred<Int>
fun somethingUsefulOneAsync() = GlobalScope.async {
doSomethingUsefulOne()
}
// The result type of somethingUsefulTwoAsync is Deferred<Int>
fun somethingUsefulTwoAsync() = GlobalScope.async {
doSomethingUsefulTwo()
}
请注意,上面的xxxAsync函数并不是suspending函数,他们可以在任何地方被调用。下面的代码是用来调用上面两个函数的代码:
// note that we don't have `runBlocking` to the right of `main` in this example
fun main() {
val time = measureTimeMillis {
// we can initiate async actions outside of a coroutine
val one = somethingUsefulOneAsync()
val two = somethingUsefulTwoAsync()
// but waiting for a result must involve either suspending or blocking.
// here we use `runBlocking { ... }` to block the main thread while waiting for the result
runBlocking {
println("The answer is ${one.await() + two.await()}")
}
}
println("Completed in $time ms")
}
上面这种编码风格仅用来展示,因为在其他语言中,这是一种很流行的编码方式,但是在kotlin中这种编码风格是强烈不推荐的。
还记得前几章有讲过一个词结构化并发
或者英文原版structured concurrency
,啥意思呢,就是当父协程取消、异常、关闭时,需要保证子协程都有被正确的取消,避免资源泄漏。
那么上面的代码呢?如果val one = somethingUsefulOneAsync()
这一行到one.await()
这一行发生任何错误,会怎样?主协程停止了,报错了,但是子协程会一直执行。
通过async做结构化异步编程
所以上面的例子,还是需要进行结构化的编码风格来做:
fun main() = runBlocking {
var a = async { doSomethingUsefulOne() }
var b = async { doSomethingUsefulTwo() }
println(a.await() + b.await())
}
suspend fun doSomethingUsefulOne(): Int {
delay(1024)
return 23
}
suspend fun doSomethingUsefulTwo(): Int {
delay(800)
return 35
}
这种编码风格,如果main中发生任何错误,所有的子协程都将被取消。
fun main() = runBlocking {
var a = async { doSomethingUsefulOne() }
var b = async { doSomethingUsefulTwo() }
println(a.await() + b.await())
}
suspend fun doSomethingUsefulOne(): Int {
delay(200)
throw RuntimeException()
return 23
}
suspend fun doSomethingUsefulTwo(): Int {
try {
//永远都不会被执行
delay(Long.MAX_VALUE)
return 35
} finally {
println("cancel.")
}
}
读者可以自己试一下上面的代码。
系列文章快速导航:
java程序员的kotlin课(一):环境搭建
java程序员的kotlin课(N):coroutines基础
java程序员的kotlin课(N+1):coroutines 取消和超时
java程序员的kotlin课(N+2):suspending函数执行编排