这部分讲述组合挂起函数的不同方法
假设我们有两个在别处定义的挂起函数,它们做一些有用的东西,像某种服务请求或者计算。我们假设它们仅仅是有用的,但是为了说明这个例子,每个实际上仅仅延迟一秒:
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // 假设我们在这里做一些有用的事情
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // 假设我们在这里也做一些有用的事情
return 29
}
如果需要顺序调用它们,首先doSomethingUsefulOne,再是doSomethingUsefulTwo,然后计算它们的结果之和,我们应该怎么做?当我们需要用第一个函数的结果,决定我们是否需要调用或者决定怎么调用第二个函数,实际上我们需要怎么做。
我们需要一个正常的顺序调用,因为在协程中代码,就像在常规的代码中一样,默认是顺序的。如下例子通过计算执行两个挂起函数的总共实际来展示:
import kotlinx.coroutines.*
import kotlin.system.*
fun main() = runBlocking<Unit> {
//例子开始
val time = measureTimeMillis {
val one = doSomethingUsefulOne()
val two = doSomethingUsefulTwo()
println("The answer is ${one + two}")
}
println("Completed in $time ms")
//例子结束
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // 假设我们在这里做一些有用的事情
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // 假设我们在这里也做一些有用的事情
return 29
}
在这里获取完整代码
产生的结果如下:
The answer is 42
Completed in 2017 ms
如果调用doSomethingUsefulOne和doSomethingUsefulTwo两个之间没有任何依赖,我们想要更快地获取答案,并行地执行两者,该怎么办呢?这里async能帮上忙了。
概念上,async和launch很像。它开启了独立协程,它是一个轻量级线程,和其他协程一起并行地工作。区别在于,launch返回一个Job,没有带有任何结果值,然而async返回一个Deferred—一个轻量级非阻塞式future,它代表着后来要提供的一个promise。你可以在延缓值上用.await()来获取它的最后结果,但是deferred也是一个Job,所以如果需要你可以取消。[注: 如果你对于 future, promise, deferred 等概念感到困惑,可以先阅读并发 Promise 模型或其他资料了解相关概念。]
import kotlinx.coroutines.*
import kotlin.system.*
fun main() = 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")
//例子结束
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // 假设我们在这里做一些有用的事情
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // 假设我们在这里也做一些有用的事情
return 29
}
在这里获取完整代码
它产生像如下的结果:
The answer is 42
Completed in 1017 ms
这里快上两倍,因为我们有两个协程并行执行。注意,协程的并行总是显示的。
使用一个带有CoroutineStart.LAZY的可选start参数,async有一个懒选项。它仅仅当它的结果需要通过某个await或者调用start函数,才开启了协程。运行下面的例子:
import kotlinx.coroutines.*
import kotlin.system.*
fun main() = runBlocking<Unit> {
//例子开启
val time = measureTimeMillis {
val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
// 一些计算
one.start() // 开启第一个协程
two.start() // 开启第二个协程
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
//例子结束
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // 假设我们在这里做一些有用的事情
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // 假设我们在这里也做一些有用的事情
return 29
}
在这里获取完整代码
它产生像如下的结果:
The answer is 42
Completed in 1017 ms
所以,这里定义了两个协程,但是没有像上次那个例子一样执行。但是控制权给了程序员,关于确切什么时候通过调用start开启了执行。我们首先开启one,然后开启two,然后等待各个协程结束。
注意,如果我们在println里面调用await,忽略对各个线程的start,然后我们将会得到顺序的行为,因为await开启了协程执行,而且等待执行的结束,这不是预期的懒用例。当值的计算包含了挂起函数,async(start = CoroutineStart.LAZY)这个用例是标准lazy函数的替代品。
我们可以定义Async样式的函数,通过带有显式GlobalScope 引用的async协程生成器,异步调用doSomethingUsefulOne和doSomethingUsefulTwo。我们用“Async”后缀来命名这个函数,强调这个事实:它们仅仅开启异步的计算,需要使用最终的延缓值来获取结果。
// somethingUsefulOneAsync结果类型是Deferred
fun somethingUsefulOneAsync() = GlobalScope.async {
doSomethingUsefulOne()
}
// somethingUsefulTwoAsync结果类型是Deferred
fun somethingUsefulTwoAsync() = GlobalScope.async {
doSomethingUsefulTwo()
}
注意,这些xxxAsync函数不是挂起函数。它们可以在任何地方使用。然而,它们的使用总是蕴含着用调用代码的它们行为异步(这里意味着并行)执行。
如下例子展示了在协程外部的使用:
import kotlinx.coroutines.*
import kotlin.system.*
//例子开始
// 注意,在这个例子中我们没有在`main`右边的`runBlocking`
fun main() {
val time = measureTimeMillis {
// 我们可以在一个协程外面初始化async行为
val one = somethingUsefulOneAsync()
val two = somethingUsefulTwoAsync()
// 但是等待结果必须包括要么是挂起的或者阻塞式。
// 这里我们使用`runBlocking { ... }`来阻塞主线程,同时等待结果
runBlocking {
println("The answer is ${one.await() + two.await()}")
}
}
println("Completed in $time ms")
}
//例子结束
fun somethingUsefulOneAsync() = GlobalScope.async {
doSomethingUsefulOne()
}
fun somethingUsefulTwoAsync() = GlobalScope.async {
doSomethingUsefulTwo()
}
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
}
在这里获取完整代码
这个async函数程序样式在这里仅仅是为了说明,因为它在其他编程语言中时很受欢迎。Kotlin协程使用这个样式,是强烈不推荐的,理由下面会解释。
考虑下,在val one = somethingUsefulOneAsync() 这一行 和 one.await()表达式之间,在代码中有某个逻辑错误,程序抛出一个异常,这个程序执行的操作终止了。通常,一个全局的错误处理器会捕获这个异常,为了开发者记录和报告这个错误,但是另外这个程序继续其他的操作。但是这里somethingUsefulOneAsync仍旧在后台运行,虽然有这个事实,已经初始化的这个操作终止了。这个问题不会在结构化并行上不会发生,就像下一部分所展示。
让我们把Concurrent using async作为例子,抽取一个函数,并行地执行doSomethingUsefulOne和doSomethingUsefulTwo,然后返回它们结果之和。因为async协程生成器是在CoroutineScope上定义的一个扩展,我们需要把它包括在作用域里面,也就是coroutineScope函数提供的:
suspend fun concurrentSum(): Int = coroutineScope {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
one.await() + two.await()
}
这是方式是,如果在concurrentSum代码里面发生错误了,它会抛出一个异常,所有这个作用域里面启动的协程都会被取消。
import kotlinx.coroutines.*
import kotlin.system.*
fun main() = runBlocking<Unit> {
//例子开始
val time = measureTimeMillis {
println("The answer is ${concurrentSum()}")
}
println("Completed in $time ms")
//例子结束
}
suspend fun concurrentSum(): Int = coroutineScope {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
one.await() + two.await()
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // 假设我们在这里做一些有用的事情
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // 假设我们在这里也做一些有用的事情
return 29
}
在这里获取完整代码
从上面main函数的结果看来,我们仍旧并行地执行两个协程:
The answer is 42
Completed in 1017 ms
取消总是通过协程层级传播:
import kotlinx.coroutines.*
fun main() = runBlocking<Unit> {
try {
failedConcurrentSum()
} catch(e: ArithmeticException) {
println("Computation failed with ArithmeticException")
}
}
suspend fun failedConcurrentSum(): Int = coroutineScope {
val one = async<Int> {
try {
delay(Long.MAX_VALUE) // 模拟非常耗时的计算
42
} finally {
println("First child was cancelled")
}
}
val two = async<Int> {
println("Second child throws an exception")
throw ArithmeticException()
}
one.await() + two.await()
}
在这里获取完整代码
注意,子字协程失败时,第一个async和等待着的父协程也被取消了:
Second child throws an exception
First child was cancelled
Computation failed with ArithmeticException