协程Job的取消,你真的用对了吗

前言

我们知道,调用协程的lifecycleScope的launch方法后会生成一个Job对象,Job可以调用cancel()方法来取消,也可以由lifecycle宿主在生命周期结束时自行取消。但job取消后,并不代表其后面的代码都不执行了,在老油条同事的代码里也发现了同样的问题,cancel后并没有真正停掉后台的任务

结论

协程Job的cancel()方法并不会立即中断后续代码的执行,只是将任务状态isActive改为false。只有当执行下一个可取消的suspend方法时,才会抛出一个CancellationException,停掉后面的代码。

这意味着,如果一个Job在任务过程中不存在一个可取消suspend方法的调用,那么直到任务结束都不会停止,即使是调用了cancel()方法。

fun jobTest() {
    runBlocking {
        val job1 = launch(Dispatchers.IO) {
            Log.d(TAG, "job1 start")
            Thread.sleep(2_000)
            Log.d(TAG, "job1 finish")
        }
        val job2 = launch {
            Log.d(TAG, "job2 start")
            delay(2_000)
            Log.d(TAG, "job2 finish")
        }
        delay(1000)
        job1.cancel()
        job2.cancel()
    }
}
2024-06-10 23:05:37.407 21238-21272 JobTest    D  job1 start
2024-06-10 23:05:37.407 21238-21327 JobTest    D  job2 start
2024-06-10 23:05:39.407 21238-21272 JobTest    D  job1 finish

如上述示例中,job1跟job2都调用了cancel()方法取消,但由于job1任务内没有suspend方法,job1在cancel后依然执行完了代码;而job2在第二个delay方法前取消了,后面的代码也不再执行。

虽然说协程任务的错误取消,通常情况下也不会导致逻辑出错或者业务异常,但还是会造成一些后台资源的浪费或者内存泄漏问题。而且也由于没有太大影响,很多时候也难以被发现,像是代码刺客一样的东西在危害着项目。

如何取消协程

方式一:既然job取消后会改变任务状态,可以在代码语句中根据isActive状态决定是否继续执行,如下

lifecycleScope.launch(Dispatchers.IO) {
    val job = launch {
        Log.d(TAG, "job start")
        while (isActive) {
            //..
        }
        Log.d(TAG, "job finish")
    }
    delay(1000)
    job.cancel()
    Log.d(TAG, "job cancel")
}
2024-06-10 23:54:46.430  4094-4353  JobTest        D  job start
2024-06-10 23:54:47.434  4094-4330  JobTest        D  job cancel
2024-06-10 23:54:47.434  4094-4353  JobTest        D  job finish

方式二:在代码执行语句中有suspend修饰的挂起方法,在协程取消后执行到suspend方法会抛出异常,从而停止协程job

lifecycleScope.launch(Dispatchers.IO) {
    val job = launch {
        Log.d(TAG, "job start")
        while (true) {
            delay(1)
        }
        Log.d(TAG, "job finish")
    }
    job.invokeOnCompletion {
        Log.d(TAG, "invokeOnCompletion:$it")
    }
    delay(1000)
    job.cancel()
    Log.d(TAG, "job cancel")
}
2024-06-10 23:59:22.531 10172-10371 JobTest        D  job start
2024-06-10 23:59:23.536 10172-10270 JobTest        D  job cancel
2024-06-10 23:59:23.539 10172-10380 JobTest        D  invokeOnCompletion:kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelled}@3df8870

可以看到任务抛出了JobCancellationException,并且不会执行到job finish语句。

两种任务停止方式的区别在于,第二种方式因为delay()这个suspend方法抛出了异常而终止执行,第一种由于没有遇到suspend方法并不会抛出异常,可以执行到结束。

那么只要是suspend方法就一定能停止协程吗?

lifecycleScope.launch(Dispatchers.IO) {
    val job = launch {
        Log.d(TAG, "job start")
        while (true) {
            emptySuspend()
        }
        Log.d(TAG, "job finish")
    }
    job.invokeOnCompletion {
        Log.d(TAG, "invokeOnCompletion:$it")
    }
    delay(1000)
    job.cancel()
    Log.d(TAG, "job cancel")
}

private suspend fun emptySuspend() {
    return suspendCoroutine {
        it.resume(Unit)
    }
}

2024-06-11 00:04:45.144 14010-14234 JobTest        D  job start
2024-06-11 00:04:46.151 14010-14241 JobTest        D  job cancel

运行后等待数秒,发现并不会抛出异常。明明一直在调用suspend方法,任务取消后却不会响应。

事实上,普通suspend方法并不会处理cancel标志,只有suspendCancelable类型方法会在执行前判断cancel状态并抛出异常。而常见的delayemit方法都是suspendCancelable类型。

emptySuspend()方法做一个修改如下

private suspend fun emptySuspend() {
    return suspendCancellableCoroutine {
        it.resume(Unit)
    }
}

运行后发现任务可以被cancel()掉而停止

2024-06-11 00:09:11.169 17728-17872 JobTest        D  job start
2024-06-11 00:09:12.174 17728-17865 JobTest        D  job cancel
2024-06-11 00:09:12.177 17728-17872 JobTest        D  invokeOnCompletion:kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelled}@7cc1e91

协程取消原理

再简单从协程的实现原理解释一下为什么协程Job要在执行suspend方法时才能中断。

挂起方法

suspend修饰的方法称为挂起方法,需要在协程作用域才能调用。

suspend fun delaySuspend() {
    Log.d(TAG, "start delay: ")
    delay(100)
    Log.d(TAG, "delay end")
}

挂起方法会编译成Switch状态机模式,每个挂起方法都是其中一个case,每个case执行都依赖前面的case,这就是协程切换与挂起停止的原理。协程本质上是产生了一个 switch 语句,每个挂起点之间的逻辑都是一个 case 分支的逻辑。

Function1 lambda = (Function1)(new Function1((Continuation)null) {
    int label;

    @Nullable
    public final Object invokeSuspend(@NotNull Object $result) {
        byte text;
        @BlockTag1: {
            Object result;
            @BlockTag2: {
                result = IntrinsicsKt.getCOROUTINE_SUSPENDED();
                switch(this.label) {
                    case 0:
                        ResultKt.throwOnFailure($result);
                        this.label = 1;
                        if (SuspendTestKt.dummy(this) == result) {
                            return result;
                        }
                        break;
                    case 1:
                        ResultKt.throwOnFailure($result);
                        break;
                    case 2:
                        ResultKt.throwOnFailure($result);
                        break @BlockTag2;
                    case 3:
                        ResultKt.throwOnFailure($result);
                        break @BlockTag1;
                    default:
                        throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }

            text = 1;
            System.out.println(text);
            this.label = 2;
            if (SuspendTestKt.dummy(this) == result) {
                return result;
            }
        }

        text = 2;
        System.out.println(text);
        this.label = 3;
        if (SuspendTestKt.dummy(this) == result) {
            return result;
        }
    }
    text = 3;
    System.out.println(text);
    return Unit.INSTANCE;
}

@NotNull
public final Continuation create(@NotNull Continuation completion) {
    Intrinsics.checkNotNullParameter(completion, "completion");
    Function1 funcation = new <anonymous constructor>(completion);
    return funcation;
}

public final Object invoke(Object object) {
    return ((<undefinedtype>)this.create((Continuation)object)).invokeSuspend(Unit.INSTANCE);
        }
});
任务取消

任务取消后,对于suspendCancelable方法的分支,会因为取消的状态而抛出JobCancellationException,停止后续代码的执行。

如果在job中对于异常进行捕获,将可能导致任务取消失败。

lifecycleScope.launch(Dispatchers.IO) {
    val job = launch {
        Log.d(TAG, "job start")
        kotlin.runCatching {
            while (true) {
                emptySuspend()
            }
        }.onFailure {
            Log.e(TAG, "catch: $it")
        }
        Log.d(TAG, "job finish")
    }
    delay(1000)
    job.cancel()
    Log.d(TAG, "job cancel")
}
2024-06-11 00:22:22.686 25890-26199 JobTest        D  job start
2024-06-11 00:22:23.690 25890-26217 JobTest        D  job cancel
2024-06-11 00:22:23.696 25890-26199 JobTest        E  catch: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@e557022
2024-06-11 00:22:23.696 25890-26199 JobTest        D  job finish

由于捕获了JobCancellationException,导致job finish语句正常执行了。

在一些情况下,可能会由于JobCancellationException被捕获导致任务没有及时取消。因此在job内捕获异常时,选择性的过滤掉JobCancellationException,将异常再度抛出

kotlin.runCatching {
    // ...
}.onFailure {
    if (it is CancellationException) {
        throw it
    }
}
协程异常处理

协程遇到无法处理的异常后,会按照停止自身子任务-停止自身任务-停止父任务的顺序依次停掉任务,并将异常抛给父作用域。当所有作用域都无法处理异常,会抛给unCautchExceptionHandler。如果异常一直没被处理,则可能引起崩溃。

值得一提的是,由Job.cancel()方法引起的CancellationException并不会传给父Job,在cancelParent之前会被过滤掉,也就是cancel()方法只能取消自身和子协程,不会影响父协程,也不会引起程序崩溃。

作者:护城河编程大师
链接:https://juejin.cn/post/7378363694939635722
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
协程中,可以使用协程作用域中的 `cancel()` 函数来取消一个协程。当协程取消时,它会抛出一个 `CancellationException` 异常。如果在协程中捕获了这个异常,那么协程的执行会立即结束。 然而,在某些情况下,我们可能不想让协程取消,或者不想让它在取消时抛出异常。这时候我们就可以使用 `SupervisorJob` 和 `SupervisorScope` 来管理协程取消操作。 `SupervisorJob` 是一个特殊的 `Job` 类型,它会将所有子协程放入一个单独的协程作用域中,从而使它们互相独立。当一个子协程取消时,其它子协程不会受到影响。同时,如果使用 `SupervisorScope` 来启动子协程,则在取消时也不会抛出 `CancellationException` 异常。 下面是一个例子,展示了如何在协程中使用 `SupervisorJob` 和 `SupervisorScope`: ```kotlin import kotlinx.coroutines.* fun main() = runBlocking { val supervisor = SupervisorJob() val scope = CoroutineScope(coroutineContext + supervisor) val job1 = scope.launch { println("Child coroutine 1 started") delay(1000) println("Child coroutine 1 completed") } val job2 = scope.launch { println("Child coroutine 2 started") try { delay(3000) println("Child coroutine 2 completed") } catch (e: CancellationException) { // do nothing } } delay(2000) job1.cancel() job2.cancel() supervisor.children.forEach { it.join() } println("All coroutines completed") } ``` 在这个例子中,我们首先创建了一个 `SupervisorJob`,然后使用它来创建一个协程作用域。在协程作用域中,我们创建了两个子协程 `job1` 和 `job2`。在 `job1` 中,我们没有使用 `SupervisorScope`,所以在取消时会抛出 `CancellationException` 异常。而在 `job2` 中,我们使用了 `SupervisorScope`,所以在取消时不会抛出异常。 最后,我们通过遍历 `supervisor.children` 来等待所有子协程执行完毕,并输出 "All coroutines completed"。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值