Kotlin协程 —— 今天说说 launch 与 async
上文我们已经知道了,在没有CoroutineScope时,我们可以通过实现该接口,或者使用 runBlocking 方法,来使我们的程序可以调用 suspend 挂起函数。
今天我们来看看 Builders.common 下的几个构建协程函数:launch
与 async
函数
launch 函数
在上一篇文章中我们已经接触过数次 launch 函数了,他的主要作用就是在当前协程作用域中创建一个新的协程。在子协程中执行耗时任务或挂起函数时,只对子协程有影响,上文中我们提到过的这是 CoroutineScope 的原因。
1 | public fun CoroutineScope.launch( |
launch 函数的返回值是 Job,比如说当用户关闭页面时,后台请求尚未返回,但此时结果已经无关紧要了,我们可以通过Job.cancel()
函数来取消掉当前执行的协程任务。
再举个栗子?,如果我们将一些耗时任务放在子协程中处理,但是父协程需要用到子协程的结果,这时候我们该怎么办?这就是我们要介绍的 Job.join()
函数
1 | public suspend fun join() |
该函数将会挂起当前的协程直到 Job 的状态变为isCompleted
,在父线程中调用了 join 之后,将会在调用出挂起协程,直到子协程执行完成(或是取消)。
1 |
|
上述代码的执行结果为:
1 | 4321 |
这是因为第一次执行打印时job还没有执行完毕,所以 string 的值为初始值,我们调用 join 将主协程挂起之后,主协程将会一直阻塞到 launch 内的代码执行完毕,再次打印就是重新赋值后的新值。
如果我们为 launch
函数设置 CoroutineStart 参数 为 LAZY
时,join()
函数还起到启动子协程的作用。
Job的生命周期如下图所示:
处于不同生命周期时的不同状态位:
上面我们提到过取消子协程的任务只需调用 cancel 函数即可,但是这存在一个隐患,即子协程有可能在取消的过程中改变了父协程的变量状态,因此争取的取消应该是这样的:
1 | job.cancel() |
即调用取消函数后立刻在父协程挂起,直到取消成功,再继续执行,官方提供了简化方法 job.cancelAndJoin()
。
Job为什么可以被取消?
1 |
|
上述代码执行到 cancelAndJoin()
函数时,子协程的任务将会终止。但是如果我们将delay()
函数替换成 Thread.sleep()
这时你会发现,子协程没有被取消,这是因为什么呢?如果对上述代码 delay
函数进行try catch
,你会发现在调用cancel函数后,delay 函数抛出了一个JobCancellationException
异常。
在文档中有这样一句话,不是很好理解:
协程的取消是 协作 的。一段协程代码必须协作才能被取消。
这句话说白了就是整个子协程中的代码必须要是可以被取消的(所有 kotlinx.coroutines 中的挂起函数都是 可被取消的 ),这些挂起函数会检查协程的取消, 并在取消时抛出 CancellationException,从而达成取消 Job 的操作。
那么,面对代码块中没有调用这些挂起函数的情况,我们怎么才能让我们的子协程拥有可被取消的能力呢?
- 定期调用 kotlinx.coroutines 中的挂起函数,如 yield
- 显示检查协程的取消状态
方式2的代码如下:
1 |
|
要注意的一点是,这里必须为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 | public fun <T> CoroutineScope.async( |
与 launch 几乎完全相同,同样是CoroutineScope的一个扩展函数,用于开启一个新的子协程,与 launch 函数一样可以设置启动模式,不同的是它的返回值为 **Deferred
Deferred 继承自 Job 接口,但是扩展了几个函数,用于获取 async 函数的返回值。
1、await() 函数
这是一个挂起函数,返回值为 **Deferred
使用方法如下:
1 |
|
取消线程的方式与 Job 是一致的。
2、getCompleted() 函数
这是一个普通函数,用于获取协程返回值,没有 Deferred
3、getCompletionExceptionOrNull()
getCompletionExceptionOrNull() 函数用来获取已完成状态的Coroutine异常信息,如果任务正常执行完成了,则不存在异常信息,返回null。如果还没有处于已完成状态,则调用该函数同样会抛出 IllegalStateException。
总结:
launch 与 async 这两个函数大同小异,都是用来在一个 CoroutineScope 内开启新的子协程的。不同点从函数名也能看出来,launch 更多是用来发起一个无需结果的耗时任务(如批量文件删除、创建),这个工作不需要返回结果。async 函数则是更进一步,用于异步执行耗时任务,并且需要返回值(如网络请求、数据库读写、文件读写),在执行完毕通过 await() 函数获取返回值。
如何选择这两个函数就看我们自己的需求啦,比如只是需要切换协程执行耗时任务,就用 launch 函数。如果想把原来的回调式的异步任务用协程的方式实现,就用 async 函数。