Kotlin协程的那些事 ---- 协程上下文和异常的传播

本文深入探讨了Kotlin协程的取消机制,特别是CPU密集型任务的处理。介绍了如何在协程中通过判断isActive或使用ensureActive来取消循环。同时,详细阐述了协程上下文的组成,包括Job、Dispatchers、CoroutineName和CoroutineExceptionHandler,并展示了它们在子协程中的继承关系。此外,文章还讨论了异常的传播,包括launch和async的异常捕获差异,以及如何利用CoroutineExceptionHandler捕获未被捕获的异常。最后,提到了SupervisorJob和supervisorScope在异常处理中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

协程在执行的过程,可以通过取消协程作用域来取消协程,这种情况下会抛出异常,但是已经被静默处理掉,因此用户无感知

1 不可取消的任务

在kotlin中,所有的挂起函数 withContext、delay等,都是可以被取消的,但是像CPU密集型任务,是不可被取消的,例如循环

var i = 0
val job = viewModelScope.launch {
	//这里判断Job的生命周期
    while (i < 5 /* && isActive  ensureActive*/) {

        i += 1
        delay(1000)
        Log.e("TAG", "index ---- $i++")
    }
}


delay(2000)
Log.e("TAG", "我要取消了")
job.cancelAndJoin()

如果想要取消CPU密集型任务,因为在执行Job的cancel方法之后,isActive属性就是变为false,那么可以通过判断isActive 或者 ensureActive 来 break循环

2 协程上下文

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
}

在构建协程作用域中,第一个参数就是协程上下文CoroutineContext,默认就是EmptyCoroutineContext

2.1 协程上下文的组成

协程上下文是由Job,Dispatchers,CorotinueName,CorotinueExceptionHandler组成,其中Job用来控制协程的生命周期,Dispatchers用来分配协程执行的线程,CorotinueName代表协程的名字,在调试的时候,可以查看目标协程的状态,CorotinueExceptionHandler用来捕获协程中的异常

val job = viewModelScope.launch(Dispatchers.IO + Job() + CoroutineName("test") ) {
    Log.e("TAG","${coroutineContext}")
}

打印:[CoroutineName(test), StandaloneCoroutine{Active}@6f35b52, Dispatchers.IO]

2.2 协程上下文的继承

子协程的创建,会创建一个新的Job对象,其他像Dispatchers,CorotinueName,都会继承自父协程

val job = viewModelScope.launch(Dispatchers.IO + Job() + CoroutineName("test") ) {

    Log.e("TAG","${coroutineContext}")
	//子协程
    launch {
        Log.e("TAG","${coroutineContext}")
    }
}

打印: [CoroutineName(test), StandaloneCoroutine{Active}@6f35b52, Dispatchers.IO]
	  [CoroutineName(test), StandaloneCoroutine{Active}@edd0e23, Dispatchers.IO]

其中可以看出,除了Job对象不同之外,其他跟父协程一致,如果在子协程中声明新的上下文,那么就会覆盖

val job = viewModelScope.launch(Dispatchers.IO + Job() + CoroutineName("test") ) {
    Log.e("TAG","${coroutineContext}")
    //子协程声明了新的Dispatchers
    launch (Dispatchers.Main){
        Log.e("TAG","${coroutineContext}")
    }
}

打印: [CoroutineName(test), StandaloneCoroutine{Active}@6f35b52, Dispatchers.IO]
	  [CoroutineName(test), StandaloneCoroutine{Active}@edd0e23, Dispatchers.Main]

3 异常的传播

当协程发生异常时,之前曾经介绍过的取消异常是被静默处理掉了,但是如果抛出其他的异常,需要手动处理异常

3.1 launch 和 async的异常捕获

launch和async是创建协程的2种方式,像launch创建的协程,异常是自动传播的,只要发生了异常,就会第一时间抛出;而async则是属于用户消费异常,只有用户调用await才能抛出异常

因此,launch和async在捕获异常的时候是不同的,launch需要在协程体内捕获异常,而async需要在调用await的时候,才能捕获异常

viewModelScope.launch(Dispatchers.IO + Job() + CoroutineName("test") ) {
    print(123)
    throw  ArrayIndexOutOfBoundsException()
}

捕获异常的方式

viewModelScope.launch(Dispatchers.IO + Job() + CoroutineName("test") ) {
    try {
        print(123)
        throw  ArrayIndexOutOfBoundsException()
    }catch (e:Exception){
        if(e is ArrayIndexOutOfBoundsException){
            Log.e("TAG","数组越界了")
        }
    }finally {

    }
}

以上是launch方式创建的协程,如果是async创建的协程,在协程体内捕获异常是拿不到的

val job = viewModelScope.async {

    delay(500)
    throw IOException()
}

job.await()

捕获异常的方式

val job = viewModelScope.async {

    delay(500)
    throw IOException()
}

try {
    job.await()
}catch (e:IOException){
    Log.e("TAG","捕获到了异常")
}

当然,目前处理方式是在根协程中,如果是在子协程中,不管是launch还是async都会直接抛出异常,async都不需要await

viewModelScope.launch(Dispatchers.IO + Job() + CoroutineName("test") ) {
	//这里try - catch 不到这个异常
    async {
        delay(500)
        throw ArrayIndexOutOfBoundsException()
    }
}

这里还没调用await,则直接抛出了异常,这是在子协程中

3.2 协程的传播特性

当子协程中发生异常时,首先会上报自己的父协程,父协程会取消自己全部的子协程,然后再取消自己,将异常上报到自己的父协程

这个过程中,通过try-catch是捕获不到异常的,异常还是会抛出而且上报到父协程,父协程会把这些协程全部取消,这就是launch和async无法捕获的异常,如果想要捕获这些异常,就得使用协程上下文中的CorotinueExceptionHandler

val handler = CoroutineExceptionHandler { _, throwable ->
	//异常聚合  处理的异常会依附在上一个异常
   Log.e("TAG","捕获了异常 ---- ${throwable.cause}")
}

viewModelScope.launch(Dispatchers.IO + Job() + CoroutineName("test") + handler ) {

   try {
       launch {
           delay(500)
           throw ArrayIndexOutOfBoundsException()
       }
   }catch (e:Exception){}
}

像子协程中抛出了异常,如果不加CoroutineExceptionHandler,那么就会取消全部的字协程,然后加上 CoroutineExceptionHandler 就能捕获这个异常,而且不会取消全部的子协程

除了上述的方式,可以通过SupervisorJob创建一个协程作用域或者使用supervisorScope创建协程作用域,这种方式我试过了,好像该是会取消父协程,我是在ViewModel中使用的,你们自己可私下试一下

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Awesome_lay

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值