一、协程异常
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 的区别,如下:[重要]
-
SupervisorJob 作用于作用域时,该作用域下的所有协程互不影响。协程体内部的子协程抛出的异常会相互影响 (这是由于子协程在创建的时候,会重新创建一个 Job,会覆盖父协程的 SupervisorJob)
-
SupervisorJob 作用于协程时,该协程及其所有的子协程抛出的异常需要自己处理,不会影响到其它的协程
-
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
对于协程的异常处理的捕获,总结如下:
- CoroutineExceptionHandler 适用于 launch 构建的协程
- CoroutineExceptionHandler 作用于 CoroutineScope 中,或者根协程
- 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;
}
}
执行的流程如下:
- 初始化时 label = 0,进入分支后重置 label=1(恢复时进入分支 label = 1 的分支)
- 随后调用 suspendFunction(),存在挂起点,命中判断逻辑后直接 return(suspendFunction(var10000) == var2)
- 采用事件循环,获取协程的状态,挂起函数恢复后,进入 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)
}
}
}