Kotlin协程 —— 今天说说 launch 与 async

上文我们已经知道了,在没有CoroutineScope时,我们可以通过实现该接口,或者使用 runBlocking 方法,来使我们的程序可以调用 suspend 挂起函数。

今天我们来看看 Builders.common 下的几个构建协程函数:launchasync 函数

launch 函数

在上一篇文章中我们已经接触过数次 launch 函数了,他的主要作用就是在当前协程作用域中创建一个新的协程。在子协程中执行耗时任务或挂起函数时,只对子协程有影响,上文中我们提到过的这是 CoroutineScope 的原因。

1
2
3
4
5
6
7
8
9
10
11
12
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}

launch 函数的返回值是 Job,比如说当用户关闭页面时,后台请求尚未返回,但此时结果已经无关紧要了,我们可以通过Job.cancel() 函数来取消掉当前执行的协程任务。

再举个栗子?,如果我们将一些耗时任务放在子协程中处理,但是父协程需要用到子协程的结果,这时候我们该怎么办?这就是我们要介绍的 Job.join() 函数

1
public suspend fun join()

该函数将会挂起当前的协程直到 Job 的状态变为isCompleted ,在父线程中调用了 join 之后,将会在调用出挂起协程,直到子协程执行完成(或是取消)。

1
2
3
4
5
6
7
8
9
10
11
@Test
fun testjob()= runBlocking {
var string = "4321"
val job = launch {
delay(3000)
string = "1234"
}
println(string)
job.join()
println(string)
}

上述代码的执行结果为:

1
2
4321
1234

这是因为第一次执行打印时job还没有执行完毕,所以 string 的值为初始值,我们调用 join 将主协程挂起之后,主协程将会一直阻塞到 launch 内的代码执行完毕,再次打印就是重新赋值后的新值。

如果我们为 launch 函数设置 CoroutineStart 参数 为 LAZY 时,join() 函数还起到启动子协程的作用。

Job的生命周期如下图所示:
Job生命周期
处于不同生命周期时的不同状态位:
Job的状态
上面我们提到过取消子协程的任务只需调用 cancel 函数即可,但是这存在一个隐患,即子协程有可能在取消的过程中改变了父协程的变量状态,因此争取的取消应该是这样的:

1
2
job.cancel()
job.join()

即调用取消函数后立刻在父协程挂起,直到取消成功,再继续执行,官方提供了简化方法 job.cancelAndJoin()

Job为什么可以被取消?

1
2
3
4
5
6
7
8
9
10
11
@Test
fun test1()= runBlocking {
val job = launch {
repeat(10) {
delay(500L)
println(it)
}
}
delay(1000L)
job.cancelAndJoin()
}

上述代码执行到 cancelAndJoin() 函数时,子协程的任务将会终止。但是如果我们将delay() 函数替换成 Thread.sleep() 这时你会发现,子协程没有被取消,这是因为什么呢?如果对上述代码 delay 函数进行try catch,你会发现在调用cancel函数后,delay 函数抛出了一个JobCancellationException异常。

在文档中有这样一句话,不是很好理解:

协程的取消是 协作 的。一段协程代码必须协作才能被取消。

这句话说白了就是整个子协程中的代码必须要是可以被取消的(所有 kotlinx.coroutines 中的挂起函数都是 可被取消的 ),这些挂起函数会检查协程的取消, 并在取消时抛出 CancellationException,从而达成取消 Job 的操作。

那么,面对代码块中没有调用这些挂起函数的情况,我们怎么才能让我们的子协程拥有可被取消的能力呢?

  1. 定期调用 kotlinx.coroutines 中的挂起函数,如 yield
  2. 显示检查协程的取消状态

方式2的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
fun test2()= runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (isActive) { // 可以被取消的计算循环
// 每秒打印消息两次
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1000L)
job.cancelAndJoin()
}

要注意的一点是,这里必须为launch函数指定 CoroutineContext,且不能为 Dispatchers.Unconfined 。

资源释放

那么如果我们需要在协程任务取消时,释放一些资源应该如何处理(比如输入输出流的关闭等)?这里我们可以使用 try{....} finally{.....} 表达式来处理,或者使用 use 函数。

use函数是 kotlin 的一个高阶扩展函数,凡是实现了Closeable 的对象都可以使用 use 函数,从而省去异常后的资源释放。可以参考阅读:https://blog.csdn.net/qq_33215972/article/details/79762878

运行不可取消的代码块

如果我们在释放资源后仍需要调用部分挂起函数应该怎么办呢?很简单,只需要调用 withContext(NonCancellable) {……} 来运行不可取消的代码即可。

需要注意,不同于 launch 函数与 async 函数,withContext 函数是一个挂起函数,也就是说他只能在一个协程中调用,并且还会挂起当前调用的协程,直至其内部代码运行完毕,所以一般 withContext 函数在协程内部被用于切换不同线程,如执行耗时任务完毕得到返回值后,切换到 UI 线程,将数据显示到 View 上。

async 函数

老规矩,先看源码:

1
2
3
4
5
6
7
8
9
10
11
12
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine<T>(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}

与 launch 几乎完全相同,同样是CoroutineScope的一个扩展函数,用于开启一个新的子协程,与 launch 函数一样可以设置启动模式,不同的是它的返回值为 **Deferred**。简单理解的话,这就是一个带返回值的 launch 函数!

Deferred 继承自 Job 接口,但是扩展了几个函数,用于获取 async 函数的返回值。

1、await() 函数
这是一个挂起函数,返回值为 **Deferred**,T 为协程的返回值。
使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
fun testAsync()= runBlocking {
val deferred = async(Dispatchers.IO) {
//此处是一个耗时任务
delay(3000L)
"ok"
}
//此处继续执行其他任务
//..........
val result = deferred.await() //此处获取耗时任务的结果,我们挂起当前协程,并等待结果
withContext(Dispatchers.Main){
//挂起协程切换至UI线程 展示结果
println(result)
}
}

取消线程的方式与 Job 是一致的。

2、getCompleted() 函数
这是一个普通函数,用于获取协程返回值,没有 Deferred 进行包装。如果协程任务还没有执行完成则会抛出 IllegalStateException ,如果任务被取消了也会抛出对应的异常。所以在执行这个函数之前,可以通过 isCompleted 来判断一下当前任务是否执行完毕了。

3、getCompletionExceptionOrNull()
getCompletionExceptionOrNull() 函数用来获取已完成状态的Coroutine异常信息,如果任务正常执行完成了,则不存在异常信息,返回null。如果还没有处于已完成状态,则调用该函数同样会抛出 IllegalStateException。

总结:

launch 与 async 这两个函数大同小异,都是用来在一个 CoroutineScope 内开启新的子协程的。不同点从函数名也能看出来,launch 更多是用来发起一个无需结果的耗时任务(如批量文件删除、创建),这个工作不需要返回结果。async 函数则是更进一步,用于异步执行耗时任务,并且需要返回值(如网络请求、数据库读写、文件读写),在执行完毕通过 await() 函数获取返回值。

如何选择这两个函数就看我们自己的需求啦,比如只是需要切换协程执行耗时任务,就用 launch 函数。如果想把原来的回调式的异步任务用协程的方式实现,就用 async 函数。