9

掌握Kotlin Coroutine之 Job&Deferred

 3 years ago
source link: http://blog.chengyunfeng.com/?p=1087
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

掌握Kotlin Coroutine之 Job&Deferred

作者: rain 分类: 移动 发布时间: 2019-03-08 20:05 6 0条评论

前面一节介绍了 Coroutine 的 scope 概念以及 CoroutineScope 上定义的各种创建不同应用场景 Coroutine 的扩展函数。这一节来介绍 Coroutine 如何取消以及 Coroutine 的超时处理。

Coroutine 既然是异步操作,所以当不需要的时候需要及时取消以便释放系统资源;同时异步操作可能用时很久,需要有个超时的概念,当等候一定时间后可以放弃继续等待,做其他操作。

Coroutine 通过 coroutine builder 函数创建后,返回的是一个代表所创建Coroutine的实例,所以本节主要介绍上节各种 coroutine builder 函数返回的对象,通过这些对象可以实现取消和超时以及其他更丰富的操作。

CoroutineScope.launch 函数返回的是一个 Job 对象,代表一个异步的任务。Job 具有生命周期并且可以取消。 Job 还可以有层级关系,一个Job可以包含多个子Job,当父Job被取消后,所有的子Job也会被自动取消;当子Job被取消或者出现异常后父Job也会被取消。

除了通过 CoroutineScope.launch 来创建Job对象之外,还可以通过 Job() 工厂方法来创建该对象。默认情况下,子Job的失败将会导致父Job被取消,这种默认的行为可以通过 SupervisorJob 来修改。

一个 Job 具有如下的几种状态:

image

一般而言,job 创建后都处于 active 状态,表示这个 Job 已经被创建并且被启动了。 通过 coroutine builder 函数的 start 参数可以修改这个状态, 比如如果使用 CoroutineStart.LAZY 作为 start 参数,则创建的 job 处于 new 状态,这个时候需要通过调用 job 的 start 或者 join 函数来把该 job 转换为 active 状态。

处于 active 状态的 job 表示 Coroutine 正在执行。 如果执行过程中抛出了异常则会把该 job 标记为 cancelling 状态。除此之外,还可以通过调用 cancel 函数来把 job 转换为 cancelling 状态。然后当 job 完成后就处于 cancelled 状态。

下图是 job 各种状态之间的转换示意:

image

具有多个子 job 的父job 会等待所有子job完成(或者取消)后,自己才会执行完成。

在Coroutine内,通过 coroutineContext 也可以访问该 Coroutine 的job 对象, coroutineContext[Job.Key] 或者 coroutineContext[Job]。等后面介绍到 coroutineContext 的时候再来详细了解细节。

下面介绍 Job 接口定义的一些常用的函数,这些函数都是线程安全的,所以可以直接在其他 Coroutine 中调用。

start

调用该函数来启动这个 Coroutine,如果当前 Coroutine还没有执行调用该函数返回 true,如果当前 Coroutine 已经执行或者已经执行完毕,则调用该函数返回 false。

cancel

调用该函数来取消这个 Coroutine 的执行。

invokeOnCompletion

通过这个函数可以给 job 设置一个完成通知,当 job 执行完成的时候会执行这个通知函数。 回调的通知对象类型为:typealias CompletionHandler = (cause: Throwable?) -> Unit.

需要注意的是,这个 CompletionHandler 回调是同步执行的。如果 job 已经完成了,则会立刻调用 CompletionHandler 。 CompletionHandler 参数代表了 job 是如何执行完成的。 cause 有下面三种情况:
– 如果 job 是正常执行完成的,则 cause 参数为 null
– 如果 job 是正常取消的,则 cause 参数为 CancellationException 对象。这种情况不应该当做错误处理,这是任务正常取消的情形。所以一般不需要在错误日志中记录这种情况。
– 其他情况表示 job 执行失败了。

这个函数的返回值为 DisposableHandle 对象,如果不再需要监控 job 的完成情况了, 则可以调用 DisposableHandle.dispose 函数来取消监听。如果 job 已经执行完了, 则无需调用 dispose 函数了,会自动取消监听。

join 函数和前面三个函数不同,这是一个 suspend 函数。所以只能在 Coroutine 内调用。

这个函数会暂停当前的 Coroutine直到其执行完成。所以 join 函数一般用来在另外一个 Coroutine 中等待 job 执行完成后继续执行。当 job 执行完成后, job.join 函数恢复,这个时候 job 这个任务已经处于完成状态了,而调用 job.join 的Coroutine还继续处于 activie 状态。
下面是一个演示 join 函数的示例:

class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val onComplete: CompletionHandler = { Log.e(TAG, "CompletionHandler: $it") }
        val job: Job = launch(BG, CoroutineStart.LAZY) {
            for (x in 1..3) {
                delay(1000)
                Log.d(TAG, "inside launch x is $x")
        Log.d(TAG, "onCreate() called")
        job.invokeOnCompletion(onComplete)
        launch {
            delay(3000)
            Log.e(TAG, "launch before join isActive ${job.isActive} , this is $isActive")
            job.join()
            Log.e(TAG, "launch after join isCompleted ${job.isCompleted} , this is $isActive")
        fab.setOnClickListener { view ->
            // 点击 FAB 按钮可以取消
            launch { job.cancel() }

上面代码执行 log 如下:

03-02 17:49:38.001 24202-24202 D/ onCreate() called
03-02 17:49:41.041 24202-24202 E/ launch before join isActive false , this is true
03-02 17:49:42.049 24202-24242 D/ inside launch x is 1
03-02 17:49:43.050 24202-24242 D/ inside launch x is 2
03-02 17:49:44.051 24202-24242 D/ inside launch x is 3
03-02 17:49:44.052 24202-24242 E/ CompletionHandler: null
03-02 17:49:44.053 24202-24202 E/ launch after join isCompleted true , this is true

在实现 Coroutine 中支持取消操作

调用 cancel 函数会取消一个 Coroutine,但是具体正在执行中的Coroutine 能否取消取决于这个 Coroutine 的实现。Coroutine标准库中定义的 suspending function 都是支持取消操作的(比如 delay)。当执行到这些函数的时候,都会去检测当前的Coroutine是否被取消了,如果发现被取消了则取消继续执行。如果自定义的 Coroutine中没有检测当前任务是否被取消了,则这种 Coroutine是无法取消的。 在实现 Coroutine 的时候可以通过 isActive 属性来判断当前任务是否被取消了,如果发现被取消了则停止继续执行;还可以通过调用其他标准库中的 suspending function 来处理取消。

比如下面的示例中,Coroutine 代码通过不停的判断 isActive 来支持取消操作

    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextTime = startTime
        var i = 0
        while (isActive) {
        // 在任务执行的时候,不停的判断当前任务是否处于active 状态,如果不是则停止执行
            if (System.currentTimeMillis() >= nextTime) {
                Log.e(TAG, "I'm sleeping ${i++} ...")
                nextTime += 500L
    delay(1300L)
    Log.e(TAG, "main: I'm tired of waiting!")
    // 取消任务并且等待任务执行完成, cancelAndJoin 是 Job 的一个扩展函数,同时执行 cancel 和join
    job.cancelAndJoin()
    Log.e(TAG, "main: Now I can quit.")

在实际应用中, 通常在任务执行超时后我们来取消这个任务。在 Coroutine 中超时是通过 withTimeout 和 withTimeoutOrNull 这两个函数来实现的。比如:

    launch {
        val value: String? = withTimeoutOrNull(1000L) {
            Log.d(TAG, "inside withTimeout 1")
            delay(2000)
            Log.d(TAG, "inside withTimeout 2")
            "return value"
        Log.d(TAG, "timeout value = [$value]")

上面的代码log如下:

03-02 18:12:16.992 25173-25173 D inside withTimeout 1
03-02 18:12:17.995 25173-25173 D timeout value = [null]

NonCancellable

如果有些任务你希望该任务执行完,不能被调用者取消。可以通过 NonCancellable 这个 context 单例对象来实现。也就是使用 NonCancellable 作为任务的 context,这样这个任务就是不可取消的:

Kotlin
withContext(NonCancellable) {
    // this code will not be cancelled

SupervisorJob

SupervisorJob 是一个函数,定义如下:

Kotlin
fun SupervisorJob(parent: Job? = null): Job

该函数创建了一个处于 active 状态的supervisor job。如前所述, Job 是有父子关系的,如果子Job 失败了父Job会自动失败,这种默认的行为可能不是我们期望的。比如在 Activity 中有两个子Job分别获取一篇文章的评论内容和作者信息。如果其中一个失败了,我们并不希望父Job自动取消,这样会导致另外一个子Job也被取消。

supervisor job就是这么一个特殊的 Job,里面的子Job不相互影响,一个子Job失败了,不影响其他子Job的执行。

SupervisorJob(parent:Job?) 具有一个parent参数,如果指定了这个参数,则所返回的 job 就是参数 parent 的子job。如果 parent job 失败了或者取消了,则这个 supervisor job 也会被取消。当 supervisor job 被取消后,所有 supervisor job 的子Job也会被取消。

前面 Activity 所用的 MainScope() 实现中就使用了 SupervisorJob 和一个 Main Dispatcher:

public fun MainScope(): CoroutineScope =
  ContextScope(SupervisorJob() + Dispatchers.Main)

Dispatcher 将会在下一节介绍。

Deferred

Deferred 接口就比较简单了,继承自 Job 接口,额外提供了获取 Coroutine 返回结果的方法。

由于 Deferred 继承自 Job 接口,所以 Job 相关的内容在 Deferred 上也是适用的。 Deferred 提供了额外三个函数来处理和Coroutine执行结果相关的操作。

await()

await() 函数等待这个Coroutine执行完毕并返回结果,当 Coroutine 正常执行完毕、被取消了或者出现异常执行失败的时候, await() 恢复执行。

getCompleted()

getCompleted() 函数用来获取Coroutine执行的结果。如果Coroutine还没有执行完成则会抛出 IllegalStateException ,如果任务被取消了也会抛出对应的异常。所以在执行这个函数之前,可以通过 isCompleted 来判断一下当前任务是否执行完毕了。

getCompletionExceptionOrNull()

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

通过 Job 接口提供的函数可以控制 Coroutine 的执行并查询 Coroutine 的状态,通过 Deferred 接口可以获取 Coroutine 执行的结果。

Job API 文档 https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html

Deferred API 文档 https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK