------ 从异常隔离到作用域设计,彻底讲透 Kotlin 协程的容错机制
前面几篇,我们已经讲了:
CoroutineContext
↓
Job
↓
Dispatcher
↓
launch / async
↓
Exception
↓
Structured Concurrency
到这里,其实还有一个非常经典的问题。
很多同学都会问:
SupervisorJob()
supervisorScope()
这两个东西到底有什么区别?
甚至很多人觉得:
名字差不多。
功能差不多。
随便用一个就好了。
事实上,它们虽然都带:
Supervisor
但解决的是两个完全不同的问题。
甚至可以说:
SupervisorJob
supervisorScope
是 Kotlin 协程设计思想中最容易混淆,也是最经典的一组设计。
今天就彻底讲透:
为什么 Kotlin 要设计两个 Supervisor?
一、先回忆上一篇
上一篇讲过:
普通 Job:
Parent
│
├── Child1
├── Child2
└── Child3
如果:
Child1 崩溃
那么:
Parent Cancel
↓
Child2 Cancel
↓
Child3 Cancel
结果:
全家陪葬。
例如:
coroutineScope {
launch {
throw RuntimeException()
}
launch {
delay(5000)
println("还能执行吗?")
}
}
答案:
不能。
二、Google 发现问题
很多业务根本不是这样。
例如:
首页:
用户信息
Banner
推荐商品
消息通知
四个接口。
结果:
Banner:
404
于是:
用户信息没了
推荐商品没了
消息通知没了
合理吗?
显然:
不合理。
Google 觉得:
兄弟之间
应该互不影响。
于是:
SupervisorJob()
诞生。
三、SupervisorJob 到底是什么?
很多人以为:
SupervisorJob()
是一种特殊 Job。
其实上一篇已经讲过:
Job
是 CoroutineContext 的配置项。
那么:
SupervisorJob()
本质也是:
一种 Job 配置。
只不过:
普通:
Child 崩
Parent 崩
兄弟崩
Supervisor:
Child 崩
Parent 不崩
兄弟继续。
例如:
Parent
│
├── Child1 崩
├── Child2 正常
└── Child3 正常
结果:
只有 Child1 挂。
四、为什么 ViewModelScope 用 SupervisorJob?
很多人不知道:
viewModelScope
内部 其实就是:
SupervisorJob() +
Dispatchers.Main
为什么?
例如:
页面:
请求用户
请求Banner
请求消息
请求商品
如果:
Banner:
404
总不能:
整个页面协程全部取消。
所以:
Google:
兄弟互不影响。
因此:
SupervisorJob()
非常适合:
页面业务。
五、问题来了
既然:
SupervisorJob()
这么好。
为什么还要:
supervisorScope {
}
?
六、supervisorScope 到底是什么?
注意:
上一篇讲过:
Scope
=
Job树根节点。
那么:
supervisorScope {
}
其实:
不是:
配置。
而是:
创建新的Scope。
例如:
supervisorScope {
launch {
}
launch {
}
}
内部:
自动创建:
SupervisorJob。
形成:
Supervisor Scope
│
├── Child1
├── Child2
└── Child3
特点:
兄弟互不影响。
七、终于看懂区别
一句话:SupervisorJob:
我是一个 Job。
作用:
放进 CoroutineContext。
例如:
CoroutineScope(
SupervisorJob() +
Dispatchers.Main
)
supervisorScope:
我是一个 Scope 构造器。
作用:
临时创建一个异常隔离作用域。
例如:
supervisorScope {
}
所以:
SupervisorJob
是配置。
supervisorScope
是作用域。
八、项目里到底怎么选?
这个最重要。
如果 自己创建 Scope:
例如:
Repository
Manager
SDK
推荐:
CoroutineScope(
SupervisorJob() +
Dispatchers.IO
)
如果:
当前 suspend 函数:
临时需要:
几个并发任务
互不影响
推荐:
supervisorScope {
}
例如:
suspend fun loadHome() = supervisorScope {
}
九、为什么 coroutineScope 和 supervisorScope 都存在?
例如:
coroutineScope {
}
特点:
一人犯错
全家陪葬。
适合:
必须全部成功。
例如:
支付流程
登录流程
事务流程
而:
supervisorScope {
}
特点:
互不影响。
适合:
首页数据
多个接口
多个独立任务。
十、突然和前面几篇串起来了
现在回头看:
CoroutineContext
↓
Job
↓
Dispatcher
↓
launch
↓
Exception
↓
Scope
↓
Supervisor
你会发现:
Google 从头到尾都在做一件事:
控制异常传播。
普通:
异常向上传播。
Supervisor:
异常到此为止。
这就是:
Kotlin 协程容错设计。
十一、最终总结
如果让我一句话解释:
SupervisorJob()
我会说:
一种 Job 配置。
用于隔离兄弟协程异常。
如果让我解释:
supervisorScope {
}
我会说:
一种 Scope 构造器。
用于创建临时异常隔离作用域。
如果让我解释:
为什么要设计两个 Supervisor?
我会说:
一个负责配置运行环境。
一个负责构建作用域。
它们解决的问题不同,
但设计思想一致:
异常不要无限传播。
下篇预告
到这里:
CoroutineContext
✓
Job
✓
Dispatcher
✓
launch / async / withContext
✓
Exception
✓
Structured Concurrency
✓
Supervisor
✓
整个协程运行模型已经完整。
但是还有一个困扰无数 Kotlin 开发者的问题:
suspend 到底是什么?
为什么 suspend 不是开启协程?
delay()
join()
await()
collect()
为什么都是 suspend?
下一篇我们继续:
《Kotlin 协程设计思想(八):suspend 到底是什么?为什么 suspend 不是开启协程?》
从 Continuation、状态机到协程恢复机制,彻底讲透 Kotlin 协程真正的底层原理。