Kotlin 协程-进阶篇

17 阅读5分钟

一、协程异常

1. 阻断协程异常传播

协程的异常是可以传播的,子协程的异常会传递给父协程,父协程出现异常,则会取消其它的子协程。

SupervisorJob 可以用来阻止协程向父协程以及兄弟协程传播,先来看使用 Job 创建的协程,如下代码:

fun main() {
    runBlocking {
        val scope = CoroutineScope(Job())
        val job1 = scope.async(Dispatchers.IO) {
            println("job1")
            delay(1000)
            error("发生错误!!!!")
        }
        val job2 = scope.async(Dispatchers.IO) {
            delay(2000)
            println("job2")
        }
        job2.await()
        job1.await()
    }
}

输出如下:
job1
Exception in thread "main" kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=JobImpl{Cancelled}@376b4233
Caused by: java.lang.IllegalStateException: 发生错误!!!!

输出符合预期,job2 没有输出。在创建 CoroutineScope 的时候,指定它的 Job 为 SupervisorJob, job2 就能够正常输出。

那么请看下面这段代码:

fun main() {
    runBlocking {
        val scope = CoroutineScope(SupervisorJob())
        scope.launch {
            val job1 = async(Dispatchers.IO) {
                println("job1")
                delay(1000)
                error("发生错误!!!!")
            }
            val job2 = async(Dispatchers.IO) {
                delay(2000)
                println("job2")
            }
            job2.await()
            job1.await()
        }.join()
    }
}

输出如下:
job1 
Exception in thread "main" kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=JobImpl{Cancelled}@376b4233
Caused by: java.lang.IllegalStateException: 发生错误!!!!

在 scope.launch 的协程中,通过 async 的方式创建了子协程,子协程抛出的异常还是被传递到了父协程,从而导致父协程取消了所有的子协程。

接下来看 supervisorScope

fun main() = runBlocking {
    val scope = CoroutineScope(Job())
    scope.launch {
        supervisorScope {
            val job1 = async(Dispatchers.IO) {
                println("job1")
                delay(1000)
                error("发生错误!!!!")
            }
            val job2 = async(Dispatchers.IO) {
                delay(2000)
                println("job2")
            }
            job2.await()
            job1.await()
        }
    }.join()
}

输出如下:
job1
job2
Exception in thread "DefaultDispatcher-worker-1" java.lang.IllegalStateException: 发生错误!!!!

可以看到,子协程抛出的异常在 supervisorScope 中并没有影响到其它的子协程。关于和 SupervisorJob 的区别,如下:[重要]

  1. SupervisorJob 作用于作用域时,该作用域下的所有协程互不影响。协程体内部的子协程抛出的异常会相互影响 (这是由于子协程在创建的时候,会重新创建一个 Job,会覆盖父协程的 SupervisorJob)

  2. SupervisorJob 作用于协程时,该协程及其所有的子协程抛出的异常需要自己处理,不会影响到其它的协程

  3. supervisorScope 作用域内所有的子协程都不会相互影响

2. 优雅捕获异常

fun main() = runBlocking {
    val scope = CoroutineScope(SupervisorJob())
    scope.launch {
        val job1 = async(SupervisorJob()) {
            println("job1")
            delay(1000)
            error("错误!!!!")
        }
        val job2 = async {
            delay(2000)
            println("job2")
        }
        job2.await()
        job1.await()
    }.join()
}

输出如下:
job1
job2
Exception in thread "DefaultDispatcher-worker-1" java.lang.IllegalStateException: 错误!!!!

Job1 指定了 SupervisorJob,因此 Job2 能够正常输出。但是 Job1 也抛出了异常,仍然会导致应用程序 Crash。

捕获协程的 Crash 可以通过 try/catch 机制或者指定 CoroutineExceptionHandler。示例代码如下:

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { coroutineContext, throwable ->
        println("CoroutineExceptionHandler is handler")
    }
    val scope = CoroutineScope(SupervisorJob() + handler)
    scope.launch {
        val job1 = async(SupervisorJob()) {
            println("job1")
            delay(1000)
            error("错误!!!!")
        }
        val job2 = async {
            delay(2000)
            println("job2")
        }
        job2.await()
        job1.await()
    }.join()
}

输出如下:
job1
job2
CoroutineExceptionHandler is handler

对于协程的异常处理的捕获,总结如下:

  1. CoroutineExceptionHandler 适用于 launch 构建的协程
  2. CoroutineExceptionHandler 作用于 CoroutineScope 中,或者根协程
  3. try / catch 只能捕获当前协程在当前调用点抛出的异常

二、协程作用域理解

对于协程作用域的理解,就需要理解协程的工作原理。整体上来说,在 JVM 或者 Android ART 虚拟机的背景下,协程并不是什么轻量级的线程,那 Kotlin 协程是什么呢?

Kotlin 借助 有限状态机Continuation,实现挂起和恢复的能力,把异步的调用转换成同步的写法。本质上它是 Kotlin 团队为我们封装的线程调度框架。

1. 协程状态机

fun main() {
    val scope = CoroutineScope(Dispatchers.IO)
    scope.launch {
        suspendFunction()
    }
}

suspend fun suspendFunction() {
    delay(1000)
}

基于上面的 Kotlin 代码,反编译成 Java 代码之后,如下:

public final class AKt {
   public static final void main() {
      CoroutineScope scope = CoroutineScopeKt.CoroutineScope((CoroutineContext)Dispatchers.getIO());
      BuildersKt.launch$default(scope, (CoroutineContext)null, (CoroutineStart)null, new Function2((Continuation)null) {
         int label;

         public final Object invokeSuspend(Object $result) {
            Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            switch (this.label) {
               case 0:
                  ResultKt.throwOnFailure($result);
                  Continuation var10000 = (Continuation)this;
                  this.label = 1;
                  if (AKt.suspendFunction(var10000) == var2) {
                     return var2;
                  }
                  break;
               case 1:
                  ResultKt.throwOnFailure($result);
                  break;
               default:
                  throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }

            return Unit.INSTANCE;
         }

         public final Continuation create(Object value, Continuation $completion) {
            return (Continuation)(new <anonymous constructor>($completion));
         }

         public final Object invoke(CoroutineScope p1, Continuation p2) {
            return ((<undefinedtype>)this.create(p1, p2)).invokeSuspend(Unit.INSTANCE);
         }

         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke(Object p1, Object p2) {
            return this.invoke((CoroutineScope)p1, (Continuation)p2);
         }
      }, 3, (Object)null);
   }

   @Nullable
   public static final Object suspendFunction(@NotNull Continuation $completion) {
      Object var10000 = DelayKt.delay(1000L, $completion);
      return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE;
   }

   // $FF: synthetic method
   public static void main(String[] args) {
      main();
   }
}

重点来看协程状态机,简化后代码如下:

new Function2((Continuation)null) {
    int label;
    public final Object invokeSuspend(Object $result) {
        Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
        switch (this.label) {
            case 0:
                ResultKt.throwOnFailure($result);
                Continuation var10000 = (Continuation)this;
                this.label = 1;
                if (suspendFunction(var10000) == var2) {
                    return var2;
                }
                break;
            case 1:
                ResultKt.throwOnFailure($result);
                break;
            default:
                throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
        }
        return Unit.INSTANCE;
    }
}

执行的流程如下:

  1. 初始化时 label = 0,进入分支后重置 label=1(恢复时进入分支 label = 1 的分支)
  2. 随后调用 suspendFunction(),存在挂起点,命中判断逻辑后直接 return(suspendFunction(var10000) == var2)
  3. 采用事件循环,获取协程的状态,挂起函数恢复后,进入 label = 1 的分支

2. 协程挂起和恢复

挂起函数必须在协程作用域或者另一个挂起函数中调用。为什么呢?

suspendFunction() 进行 Java 反编译之后是这样的:

public static final Object suspendFunction(@NotNull Continuation $completion) {
   Object var10000 = DelayKt.delay(1000L, $completion);
   return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE;
}

可以看到函数新增加了一个 Continuation 类型的参数,那为什么会有这样的一个参数,它的作用是什么呢?

协程的挂起和恢复都依赖这个参数,协程体内部的临时变量,以及上下文的参数,都会被保存在 Continuation 中。

如果说协程的状态流转依赖状态机,那 Continuation 挂起和恢复的动力。两者共同作用,才使得 Kotlin 协程能够运转。

三、协程上下文

在 Kotlin 协程中,CoroutineContext 定义了协程的调度方式,异常处理的能力。

在 Kotlin 的底层实现中,CoroutineContext 是不可变的链表,但是在使用的的过程中,我们可以通过 key-value 方式进行查找。下面看一段 CoroutineContext 的核心逻辑:

public operator fun plus(context: CoroutineContext): CoroutineContext =
    // 1. 如果是 EmptyCoroutineContext,则直接返回
    if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
        context.fold(this) { acc, element ->
            // 2. 删除集合中和当前 context 重名的值
            val removed = acc.minusKey(element.key)
            if (removed === EmptyCoroutineContext) element else {
                // make sure interceptor is always last in the context (and thus is fast to get when present)
                val interceptor = removed[ContinuationInterceptor]
                // 3. 处理拦截器为空的情况
                if (interceptor == null) CombinedContext(removed, element) else {
                    val left = removed.minusKey(ContinuationInterceptor)
                    // 4.拦截器需要再最后添加,确保能够性能
                    if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                        CombinedContext(CombinedContext(left, element), interceptor)
                }
            }
        }