在协程里面有一个比较重要的知识点:结构化并发。处理结构化并发和异常时需要清楚协程之间的关系,比如父子协程,还有不同 CoroutineScope 的协程之间的关系。但在此之前,我们需要知道 [一个协程] 到底指的是什么。
在讲 [一个协程] 是指什么之前,我们先来看下 [一个线程] 指的是什么。
[一个线程] 指的是什么?
val thread = thread {
// ...
}
上面的代码很简单,我们创建了一个线程,这里的 [一个线程] 就是指的 Thread 对象,我们使用线程的步骤是:创建一个线程对象,然后用这个线程对象启动线程。
Thread 对象实现了对于 [线程] 这个抽象概念的管理,线程指的是 [在程序里单独运行的一条业务线],我们通过 Thread 对象可以完成对这个 [单独运行的业务线] 的管理。
比如可以通过 Thread 对象获取元数据、停止线程等等:
val thread = thread {} // 启动线程
thread.name // 获取线程名称
thread.interrupt() // 停止线程
Thread 对象承载了线程的管理功能,所以在这里把 Thread 对象看成 [一个线程] 就是理所当然的。
[一个协程] 指的是什么?
val scope = CoroutineScope(EmptyCoroutineContext)
val job = scope.launch {}
在上面的例子中,我们创建了一个 CoroutineScope 对象,然后调用 scope.launch 就启动了一个协程。这里的 [一个协程] 具体是 launch 的返回值 Job?还是 scope?
这里先透露下 [一个协程] 具体所指:
-
从管理协程流程的视角分析,把 Job 对象看成是一个 [协程对象],但是它只包含了跟协程的流程相关的功能
-
从父子协程的视角分析,launch 返回的 Job 或 launch 内的 CoroutineScope 都是属于 [协程对象];外层 CoroutineScope 是一个 [最顶级] 的管理器,CoroutineScope 和 launch 返回的 Job 是从属关系(例如 scope.cancel 可以取消由它调用 launch 所启动的所有协程,包括 launch 返回的 Job;job.cancel 只能取消自身及其子协程)
Job:管理协程流程的 [一个协程]
启动协程是通过 launch,我们就先从 launch 的源码分析:
Builders.common.kt
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
// 创建 LazyStandaloneCoroutine 或 StandaloneCoroutine
// 但最终函数返回的类型是 Job
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, activity = true)
coroutine.start(start, coroutine, block)
return coroutine
}
private open class StandaloneCoroutine(
parentContext: CoroutineContext,
active: Boolean
) : AbstractCoroutine<Unit>(parentContext, initParentJob = true, active = active) {
override fun handleJobException(exception: Throwable): Boolean {
handleCoroutineException(context, exception)
return true
}
}
private class LazyStandaloneCoroutine(
parentContext: CoroutineContext,
block: suspend CoroutineScope.() -> Unit
) : StandaloneCoroutine(parentContext, active = false) {
private val continuation = block.createCoroutineUnintercepted(this, this)
override fun onStart() {
continuation.startCoroutineCancellable(this)
}
}
AbstractCoroutine.kt
// 实现了 Job 和 CoroutineScope
@InternalCoroutinesApi
public abstract class AbstractCoroutine<in T>(
parentContext: CoroutineContext,
initParentJob: Boolean,
active: Boolean
) : JobSupport(active), Job, Continuation<T>, CoroutineScope {
...
}
launch 函数会创建 LazyStandaloneCoroutine 或 StandaloneCoroutine,LazyStandaloneCoroutine 也是 StandaloneCoroutine 的子类,StandaloneCoroutine 继承自 AbstractCoroutine,AbstractCoroutine 实现了 Job 和 CoroutineScope。launch 函数最终需要返回的是 Job 类型的对象。
这里就会有疑问:为啥不直接返回 StandaloneCoroutine 而是返回 Job 呢?
实际上协程为了做到责任的拆分,在使用上通过返回 Job 对象,限制我们只能用 Job 的 API 管理协程的流程。
可以使用 Job 对象操作管理协程流程:
// job 提供管理协程流程的 API
job.start() // 启动协程
job.cancel() // 取消协程
job.isActive // 协程是否活跃
job.isCancelled // 协程是否取消
job.isCompleted // 协程是否完成,协程被取消也为 true
job.parent // 该协程的父协程
job.children // 该协程的子协程
job.cancelChildren() // 取消所有该协程的子协程
从管理协程流程的视角分析,我们可以把 Job 对象看成是一个 [协程对象],但是它只包含了跟协程的流程相关的功能。
CoroutineScope:总管协程所有功能和数据的 [一个协程]
上一节提到 Job 是用来管理协程流程的,通过 launch 函数可以知道它甚至是能启动已经创建好的协程,为什么还要用 CoroutineScope 启动协程呢?
实际上 CoroutineScope 是一个 [最顶级] 的管理器,协程的所有数据和功能都能从 CoroutineScope 里面找到。
比如可以通过 CoroutineScope 获取在哪个线程执行,也就是 ContinuationInterceptor:
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(Dispatchers.IO)
val job = scope.launch {
// outer 和 inner 获取的 ContinuationInterceptor 是同一个对象
val outer = scope.coroutineContext[ContinuationInterceptor]
val inner = this.coroutineContext[ContinuationInterceptor]
println("outer: $outer")
println("inner: $inner")
}
}
输出结果:
outer:Dispatchers.IO
inner:Dispatchers.IO
如果在 launch 指定了和 scope 不同的 ContinuationInterceptor,launch 内的 CoroutineScope 获取的 ContinuationInterceptor 和外面的也会是不同的:
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(Dispatchers.IO)
val job = scope.launch(Dispathcers.Default) {
// launch 指定了 ContinuationInterceptor
// outer 和 inner 获取的 ContinuationInterceptor 不是同一个对象
val outer = scope.coroutineContext[ContinuationInterceptor]
val inner = this.coroutineContext[ContinuationInterceptor]
println("outer: $outer")
println("inner: $inner")
}
}
输出结果:
outer:Dispatchers.IO
inner:Dispatchers.Default
scope.launch 返回的 Job 对象和 launch 内的 CoroutineScope 获取的 Job 是同一个对象,也是同一个 CoroutineScope 对象:
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(Dispatchers.IO)
var innerJob: Job? = null
var innerScope: CoroutineScope? = null
val outerJob = scope.launch(Dispathcers.Default) {
// this 是 StandaloneCoroutine,即实现了 Job 也实现了 CoroutineScope
// outerJob 和 innerJob 是同一个对象
// outerJob 和 innerScope 也是同一个对象
innerJob = this.coroutineContext[Job]
innerScope = this
}
println("outerJob === innerJob: ${outerJob === innerJob}")
println("outerJob === innerScope: ${outerJob === innerScope}")
}
输出结果:
outerJob === innerJob: true
outerJob === innerScope: true
CoroutineScope 和 Job 并不是并列关系而是从属关系,CoroutineScope 是所有的大总管,Job 只负责其中协程流程相关的功能。
从父子协程的角度看,将 CoroutineScope 看成 [一个协程] 也是可以的,即可以把 launch 函数返回的 Job 看作是一个协程对象,也可以把 launch 内的 CoroutineScope 看成是一个协程对象。
线程与协程的区别
传统的 Java 线程是把所有功能都放进了 Thread,所以它没有协程这种职责拆分的概念,Thread 对象就是线程对象。
协程把功能拆开,它的完整功能是由 CoroutineScope 管理,同时它把最核心的流程相关的功能拆到了 Job 。所以在协程里我们把 Job 和 CoroutineScope 看作是协程对象都是可以的。
这是线程和协程的不同。
简单总结就是:线程所有功能都放到 Thread;协程做了更明确的职责化拆分,CoroutineScope 具备完整功能的管理,Job 负责协程流程的管理。
在平常开发中更直观的视角看协程是,launch 的大括号就是一个协程,大括号内调用 launch 启动的协程就是子协程,以此类推。
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(Dispatchers.IO)
// 直观的角度:一个大括号就是一个协程,大括号内的大括号就是子协程
scope.launch(Dispathcers.Default) {
launch {
}
}
}
总结
-
[一个线程] 指的就是 Thread 对象:Thread 对象实现了对于 [线程] 这个抽象概念的管理,线程指的是 [在程序里单独运行的一条业务线],我们通过 Thread 对象可以完成对这个 [单独运行的业务线] 的管理,Thread 对象承载了线程的管理功能
-
[一个协程] 从不同的视角分析 Job 和 CoroutineScope 都可以是协程对象:从管理协程流程的视角分析,把 Job 对象看成是一个 [协程对象],但是它只包含了跟协程的流程相关的功能;从父子协程的视角分析,launch 返回的 Job 或 launch 内的 CoroutineScope 都是属于 [协程对象];外层 CoroutineScope 是一个 [最顶级] 的管理器,CoroutineScope 和 launch 返回的 Job 是从属关系
-
线程与协程的区别是:线程所有功能都放到 Thread;协程做了更明确的职责化拆分,CoroutineScope 具备完整功能的管理,Job 负责协程流程的管理