「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」
六、CoroutineBuilder
1、launch
看下 launch 函数的方法签名。launch 是一个作用于 CoroutineScope 的扩展函数,用于在不阻塞当前线程的情况下启动一个协程,并返回对该协程任务的引用,即 Job 对象
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
launch 函数共包含三个参数:
- context。用于指定协程的上下文
- start。用于指定协程的启动方式。默认值为
CoroutineStart.DEFAULT,即协程会在声明的同时就立即进入等待调度的状态,即可以立即执行的状态。可以通过将其设置为CoroutineStart.LAZY来实现延迟启动,即懒加载 - block。用于传递协程的执行体,即希望交由协程执行的任务
可以看到 launchA 和 launchB 是并行交叉执行的
fun main() = runBlocking {
val launchA = launch {
repeat(3) {
delay(100)
log("launchA - $it")
}
}
val launchB = launch {
repeat(3) {
delay(100)
log("launchB - $it")
}
}
}
[main] launchA - 0
[main] launchB - 0
[main] launchA - 1
[main] launchB - 1
[main] launchA - 2
[main] launchB - 2
2、Job
Job 是协程的句柄。使用 launch 或 async 创建的每个协程都会返回一个 Job 实例,该实例唯一标识协程并管理其生命周期。Job 是一个接口类型,这里列举 Job 几个比较有用的属性和函数
//当 Job 处于活动状态时为 true
//如果 Job 未被取消或没有失败,则均处于 active 状态
public val isActive: Boolean
//当 Job 正常结束或者由于异常结束,均返回 true
public val isCompleted: Boolean
//当 Job 被主动取消或者由于异常结束,均返回 true
public val isCancelled: Boolean
//启动 Job
//如果此调用的确启动了 Job,则返回 true
//如果 Job 调用前就已处于 started 或者是 completed 状态,则返回 false
public fun start(): Boolean
//用于取消 Job,可同时通过传入 Exception 来标明取消原因
public fun cancel(cause: CancellationException? = null)
//阻塞等待直到此 Job 结束运行
public suspend fun join()
//当 Job 结束运行时(不管由于什么原因)回调此方法,可用于接收可能存在的运行异常
public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
Job 具有以下几种状态值,每种状态对应的属性值各不相同
| State | isActive | isCompleted | isCancelled |
|---|---|---|---|
| New (optional initial state) | false | false | false |
| Active (default initial state) | true | false | false |
| Completing (transient state) | true | false | false |
| Cancelling (transient state) | false | false | true |
| Cancelled (final state) | false | true | true |
| Completed (final state) | false | true | false |
fun main() {
//将协程设置为延迟启动
val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
for (i in 0..100) {
//每循环一次均延迟一百毫秒
delay(100)
}
}
job.invokeOnCompletion {
log("invokeOnCompletion:$it")
}
log("1. job.isActive:${job.isActive}")
log("1. job.isCancelled:${job.isCancelled}")
log("1. job.isCompleted:${job.isCompleted}")
job.start()
log("2. job.isActive:${job.isActive}")
log("2. job.isCancelled:${job.isCancelled}")
log("2. job.isCompleted:${job.isCompleted}")
//休眠四百毫秒后再主动取消协程
Thread.sleep(400)
job.cancel(CancellationException("test"))
//休眠四百毫秒防止JVM过快停止导致 invokeOnCompletion 来不及回调
Thread.sleep(400)
log("3. job.isActive:${job.isActive}")
log("3. job.isCancelled:${job.isCancelled}")
log("3. job.isCompleted:${job.isCompleted}")
}
[main] 1. job.isActive:false
[main] 1. job.isCancelled:false
[main] 1. job.isCompleted:false
[main] 2. job.isActive:true
[main] 2. job.isCancelled:false
[main] 2. job.isCompleted:false
[DefaultDispatcher-worker-2] invokeOnCompletion:java.util.concurrent.CancellationException: test
[main] 3. job.isActive:false
[main] 3. job.isCancelled:true
[main] 3. job.isCompleted:true
3、async
看下 async 函数的方法签名。async 也是一个作用于 CoroutineScope 的扩展函数,和 launch 的区别主要就在于:async 可以返回协程的执行结果,而 launch 不行
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>
通过await()方法可以拿到 async 协程的执行结果,可以看到两个协程的总耗时是远少于七秒的,总耗时基本等于耗时最长的协程
fun main() {
val time = measureTimeMillis {
runBlocking {
val asyncA = async {
delay(3000)
1
}
val asyncB = async {
delay(4000)
2
}
log(asyncA.await() + asyncB.await())
}
}
log(time)
}
[main] 3
[main] 4070
由于 launch 和 async 仅能够在 CouroutineScope 中使用,所以任何创建的协程都会被该 scope 追踪。Kotlin 禁止创建不能够被追踪的协程,从而避免协程泄漏
4、async 的错误用法
修改下上述代码,可以发现两个协程的总耗时就会变为七秒左右
fun main() {
val time = measureTimeMillis {
runBlocking {
val asyncA = async(start = CoroutineStart.LAZY) {
delay(3000)
1
}
val asyncB = async(start = CoroutineStart.LAZY) {
delay(4000)
2
}
log(asyncA.await() + asyncB.await())
}
}
log(time)
}
[main] 3
[main] 7077
会造成这不同区别是因为 CoroutineStart.LAZY不会主动启动协程,而是直到调用async.await()或者async.satrt()后才会启动(即懒加载模式),所以asyncA.await() + asyncB.await()会导致两个协程其实是在顺序执行。而默认值 CoroutineStart.DEFAULT 参数会使得协程在声明的同时就被启动了(实际上还需要等待被调度执行,但可以看做是立即就执行了),所以即使 async.await()会阻塞当前线程直到协程返回结果值,但两个协程其实都是处于运行状态,所以总耗时就是四秒左右
此时可以通过先调用start()再调用await()来实现第一个例子的效果
asyncA.start()
asyncB.start()
log(asyncA.await() + asyncB.await())
5、async 并行分解
由 suspend 函数启动的所有协程都必须在该函数返回结果时停止,因此你可能需要保证这些协程在返回结果之前完成。借助 Kotlin 中的结构化并发机制,你可以定义用于启动一个或多个协程的 coroutineScope。然后,你可以使用 await()(针对单个协程)或 awaitAll()(针对多个协程)保证这些协程在从函数返回结果之前完成
例如,假设我们定义一个用于异步获取两个文档的 coroutineScope。通过对每个延迟引用调用 await(),我们可以保证这两项 async 操作在返回值之前完成:
suspend fun fetchTwoDocs() =
coroutineScope {
val deferredOne = async { fetchDoc(1) }
val deferredTwo = async { fetchDoc(2) }
deferredOne.await()
deferredTwo.await()
}
你还可以对集合使用 awaitAll(),如以下示例所示
suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main)
coroutineScope {
val deferreds = listOf( // fetch two docs at the same time
async { fetchDoc(1) }, // async returns a result for the first doc
async { fetchDoc(2) } // async returns a result for the second doc
)
deferreds.awaitAll() // use awaitAll to wait for both network requests
}
虽然 fetchTwoDocs() 使用 async 启动新协程,但该函数使用 awaitAll() 等待启动的协程完成后才会返回结果。不过请注意,即使我们没有调用 awaitAll(),coroutineScope 构建器也会等到所有新协程都完成后才恢复名为 fetchTwoDocs 的协程。此外,coroutineScope 会捕获协程抛出的所有异常,并将其传送回调用方
6、Deferred
async 函数的返回值是一个 Deferred 对象。Deferred 是一个接口类型,继承于 Job 接口,所以 Job 包含的属性和方法 Deferred 都有,其主要就是在 Job 的基础上扩展了 await()方法