此文档用于记录本人在协程内容学习中遇到的疑难点问题,属于经验总结版,记录目的主要是为了方便后续查阅,及时捡起此部分学习内容
若文档中有错误或遗漏处,欢迎指正
1. 协程使用及其调用流程记录
val job = Job()
val coroutineBlock: suspend CoroutineScope.() -> Unit = {
// empty
}
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("exceptionHandler", "${coroutineContext[CoroutineName]} $throwable")
}
GlobalScope.launch(job + Dispatchers.Default + exceptionHandler, CoroutineStart.DEFAULT, coroutineBlock)
以上为一个常用的协程使用方式,我们在GlobalScope作用域内使用创建了一个协程并将其父协程指定为创建的job,指派Dispatchers.Default为执行位置,附加异常处理到exceptionHandler其中。其次,我们指定该协程的执行方式为CoroutineStart.DEFAULT,即默认启动即可。具体的协程执行代码块即coroutineBlock。
了解清晰其大体执行组成成分之后,我们首先来分析下其执行流程。
1.1 协程的执行流程
首先我们要明白的是,协程的一切实现均是依托于线程进行的 在有了这个实现前提后,我们开始从源码执行角度看待其流转,以下部分会将对应的代码拆分并放置到具体执行位置
launch(context, start, block) {
// 将传递的所有内容包装成一个coroutine执行协程,并开启内容执行
coroutine.start(start, coroutine, block) {
initParentJob() {
// 从传入的协程上下文中寻找Job类型子项进行父Job初始化
initParentJobInternal(parentContext[Job]) {
// 完成job的依附,此时构建函数中传入的job变成了生成的coroutine父Job
val handle = parent.attachChild(this)
}
}
start(block, receiver, this) {
// 使用的就是CoroutineStart方法,我们使用的是DEFAULT
block.startCoroutineCancellable(receiver, completion) {
createCoroutineUnintercepted(receiver, completion) {
// 此处实现为第一次调用invoke方法时的实现类调用的,此时调用者为SuspendLambda,已经实现了BaseContinuationImpl----此处的实现需要从编译后的class文件中查看,调用者为this,源码下无法看出真实调用者
if (this is BaseContinuationImpl)
// create调用的是生成的SuspendLambda中的create(),与具体的代码挂钩
create(receiver, probeCompletion)
else ....
}.intercepted() {
// 此处代码调用者仍是SuspendLambda-->ContinuationImpl(实现)
// 刚生成的SuspendLambda,他的intercepted为空,即最终调用的是上下文中的ContinuationInterceptor拦截器对应interceptContinuation。此处我们用的是Dispatchers.Default
intercepted ?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this).also { intercepted = it }
// 插入看下Dispatchers.Default
// Default-> DefaultScheduler-> ExperimentalCoroutineDispatcher->ExecutorCoroutineDispatcher-> CoroutineDispatcher
// 最终是CoroutineDispatcher的实现(全局仅有该类实现了此方法,故所有启动方式均调用的该类方法)
// 这里的continuation即最终生成的SuspendLambda代码执行块,this指的是dispatcher
// CoroutineDispatcher.interceptContinuation(continuation) {
// DispatchedContinuation(this, continuation)
// }
}.resumeCancellableWith(Result.success(Unit), onCancellation) {
...
// **这个DispatchedContinuation非常关键**
// DispatchedContinuation-> DispatchedTask-> SchedulerTask()-> Task-> Runable
// DispatchedContinuation-> Continuation
// **继承了线程任务,又继承了协程实现,并将协程体放入到内部变量中存储。他其实就是整个实现环节的核心,架起了两者之间的关联**
is DispatchedContinuation -> resumeCancellableWith(result, onCancellation) {
dispatcher.dispatch(context, this) {
// 此时使用的就是Dispatchers.Default-->DefaultScheduler进行协程执行代码分发,this即代指生成的DispatchedContinuation包裹块
ExperimentalCoroutineDispatcher.dispatch(context: CoroutineContext, block: Runnable) {
coroutineScheduler {
createScheduler() {
// 此时,默认的线程执行池已经创建完毕
CoroutineScheduler(corePoolSize, maxPoolSize, idleWorkerKeepAliveNs, schedulerName)
}
}.dispatch(block) {
// 执行的block即为包装的DispatchedContinuation包裹块,他继承DispatchedTask,最终调用其内部方法驱动协程代码块运行
// 但是在此处,仅将任务扔到队列中即可
......
// 包装下task用于后续执行
val task = createTask(block, taskContext)
val currentWorker = currentWorker()
// 将任务扔到队列中
val notAdded = currentWorker.submitToLocalQueue(task, tailDispatch)
......
// 至此,触发的流程基本就结束了,等待后续线程池调用执行了
}
}
}
}
...
}
}
}
}
}
// 现在关注下DispatchedTask的执行,看下我们的协程代码->SuspendLambda是怎么触发运行的
DispatchedTask.run() {
assert { resumeMode != MODE_UNINITIALIZED } // should have been set before dispatching
val taskContext = this.taskContext
var fatalException: Throwable? = null
try {
val delegate = delegate as DispatchedContinuation<T>
val continuation = delegate.continuation
withContinuationContext(continuation, delegate.countOrElement) {
val context = continuation.context
val state = takeState() // NOTE: Must take state in any case, even if cancelled
val exception = getExceptionalResult(state)
/*
* Check whether continuation was originally resumed with an exception.
* If so, it dominates cancellation, otherwise the original exception
* will be silently lost.
*/
val job = if (exception == null && resumeMode.isCancellableMode) context[Job] else null
if (job != null && !job.isActive) {
// 如果父job存在,校验其是否活跃
val cause = job.getCancellationException()
cancelCompletedResult(state, cause)
// 以错误进行回调结束
continuation.resumeWithStackTrace(cause)
} else {
if (exception != null) {
// 出现错误,回调到上层,用于取消协程体
continuation.resumeWithException(exception)
} else {
// 没有错误,那么回调具体内容的执行结果
continuation.resume(getSuccessfulResult(state))
}
}
}
} catch (e: Throwable) {
// This instead of runCatching to have nicer stacktrace and debug experience
// 存储执行过程中出现的异常--resumeWith切换到原有线程执行任务,抛出的异常也会被其捕获
fatalException = e
} finally {
// 一旦发生任何异常,进行异常处理--exceptionHandler、取消协程执行
val result = runCatching { taskContext.afterTask() }
handleFatalException(fatalException, result.exceptionOrNull())
}
}
基于上面的执行流程分析,我们可以简单得出以下结论:
从使用层面来看:
- job对象:执行过程中会将其作为父协程,可以用于管理整个执行模块的生命周期
- Dispatchers:协程执行模块的具体执行位置,根据此分派到不同的线程池中执行
- exceptionHandler:用于处理协程运行时产生的exception
- CoroutineStart:指派对应执行模式,区分在于不同模式下,block块执行时机不同
从运行流程中看
- 对于整个流转来说,我们想要的协程执行块(即状态机执行部分),均发生在设置的线程池中。其中关键的类为DispatchedContinuation,他本身作为一个线程对象,但是内部又包裹住协程块block用于执行协程代码
- 两次resumeWith:协程的挂起、恢复执行,均由此方法触发,其实都发生在BaseContinuationImpl的while循环中,由其触发invokeSuspend执行协程体任务。只不过两次调用时,挂起点的返回不同,使得其走了不同的逻辑。(我们可以看delay()方法,就会发现他的执行流程了)
- 第一次执行时:协程块开始了运行,直到执行到挂起点,满足挂起条件后,方法return,此次运行结束。
- 第二次执行时:挂起函数结束,传入的continuation会重新触发resumeWith方法,来到原有的中断位置。
- SuspendLambda:这个是最坑的点,如果不根据生成后的代码去看的话,中间跟代码到DispatchedContinuation判别时就走不下去了。对于KT的代码,如果内部原理包含有代码构建,有时候还需要联合构建后的代码一起查看,这样才可以搞清楚对应执行流程---毕竟最终还是用Java去执行的emmmm
2. 概念梳理
总结使用到的各类概念内容,并对其加以区分。
2.1 Continuation和completion
- Continuation:译为延续物,抽象出来的一个概念类,可以当作为执行体,他其中可以包含子执行体,内部有resumeWith方法,用于触发挂起和恢复代码执行
- completion:参数命名,可在BaseContinuationImpl的while循环中见到,循环中可以看到对执行体进行回溯,通过对其所属类型进行判别,就可以找到最顶层的协程体,触发resumeWith完成恢复。
2.2 解释:大多数业务场景下,线程进程可以看做是同步机制,而协程则是异步。
理解同步,我们可以看经典例子--从两个不同接口中请求资源并整合完进行展示。即便两个资源位置都是分开的,按照逻辑上来看也是两件独立的事情,但是由于最终的组装,使得这两个接口要变成同步去做了。从这个业务场景上来看,其实他们就是一个同步机制了。
而协程不一样的原因是,他的代码在执行到挂起处就直接return结束了,即便我们将两个不同的资源请求(两个async),扔到同一处做结果组装(调用await),它仍会在组装处进行挂起,只不过先请求结束的会先return,然后去做自己的其他任务,待最后一个资源请求完毕,会切换到组装处的代码进行恢复执行,这样的话,不同协程间的执行不和任何挂钩,完全异步。从这种角度来看,业务上协程已经将其完全异步化了。
但是,我们需要明白的是,在同一个协程上做任务的时候,他仍是同步进行的(毕竟都是在同一条线程上执行嘛)
2.3 为什么协程比线程的执行效率更高
首先,我们考虑下什么会影响效率---对于线程来说,第一个就是CPU的调度开销,第二个就是资源管理
调度开销:线程的生命周期由CPU进行管理,切换状态时,涉及到内核态/用户态的切换,会造成大量时间消耗;协程所有信息均由用户自己控制(状态机的执行其实是用户态的),涉及到的调度少
资源管理:我们明白,不同线程间的数据是不一样的,他们自身占有一块内存,用于存放运行时需要用到的数据资源,通常很大;对比协程的KB级,就很耗费时间了
2.4 async和launch的区别
本质上来说,其实两者真的没有什么区别,只是两者的返回是有差别的(async的返回Deferred继承自Job,launch的返回是Job,但是Deferrd可以携带返回值,用于获取方法返回。其他的执行方法,接受入参,基本没有多少差别。差异较大的部分其实也只有Deferred自己新加的函数
- 执行方式:对于async来说,内部的代码块在编译执行中,其实仅仅只是会进行简单代码生成,仅当执行了await函数后(此函数即Deferred自己添加的新函数),具体的block协程块才会开始执行,后续就是经典的执行流程了。对于launch来说,他的产生就是直接进入执行。
- 概念:就使用上来说,async -- > 异步;launch -- > 同步
2.5 Dispatcher
Q:多个协程可以运行到同一个线程上是指什么?
A:其实关键点就在于这个Dispatcher,当我们有了一个概念:即协程其实就是线程上执行的任务,那么这个问题就可以分解为,如何run我们的task?我们当然在同一个线程上执行多个协程任务,只不过这个时候,上面的任务是串行的而已。
当然,我们除了使用官方提供的Dispatcher,也可以自己写一个,并在我们自造的Dispatcher中去做线程管理,完成一些特殊的协程需求。
3. 协程异常处理流程--以协程体中出现的异常为例进行分析
基于我们上面的执行流程分析,我们先看以下的执行例子
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("exceptionHandler", "${coroutineContext[CoroutineName]} $throwable")
}
val childExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("exceptionHandler", "childExceptionHandler -- ${coroutineContext[CoroutineName]} $throwable")
}
val coroutineScope = CoroutineScope(Job() + CoroutineName("coroutineScope") + exceptionHandler)
GlobalScope.launch(Dispatchers.Main + CoroutineName("scope1")) {
coroutineScope.launch {
// 此处的Job会影响到其内部的子协程,当然我们还需要让协程间产生关联
launch(SupervisorJob(coroutineContext[Job]) + CoroutineName("scope2") + childExceptionHandler) {
Log.d("scope", "1--------- ${coroutineContext[CoroutineName]}")
throw NullPointerException("空指针")
}
launch(CoroutineName("scope3")) {
Log.d("scope", "2--------- ${coroutineContext[CoroutineName]}")
delay(2000)
Log.d("scope", "3--------- ${coroutineContext[CoroutineName]}")
}
Log.d("scope", "4--------- ${coroutineContext[CoroutineName]}")
}
Log.d("scope", "5--------- ${coroutineContext[CoroutineName]}")
}
// 执行结果---SupervisorJob
5--------- CoroutineName(scope1)
1--------- CoroutineName(scope2)
4--------- CoroutineName(coroutineScope)
2--------- CoroutineName(scope3)
childExceptionHandler -- CoroutineName(scope2) java.lang.NullPointerException: 空指针
3--------- CoroutineName(scope3)
切换成普通Job后---
// 执行结果--Job
5--------- CoroutineName(scope1)
1--------- CoroutineName(scope2)
4--------- CoroutineName(coroutineScope)
2--------- CoroutineName(scope3)
CoroutineName(coroutineScope) java.lang.NullPointerException: 空指针
不设置任何中间Job
// 执行结果,让coroutineScope处理异常
5--------- CoroutineName(scope1)
4--------- CoroutineName(coroutineScope)
1--------- CoroutineName(scope2)
2--------- CoroutineName(scope3)
CoroutineName(coroutineScope) java.lang.NullPointerException: 空指针
上述例子中SupervisorJob切换为Job最明显的区别就是,我们的3没有了。具体原因就是,没有了SupervisorJob的保护,处于同一个作用域下的协程直接被取消掉了。 带着这个结论,我们开始进行内容分析---开始位置即为BaseContinuationImpl中的resumeWith执行流程
BaseContinuationImpl.resumeWith(result: Result<Any?>) {
// This loop unrolls recursion in current.resumeWith(param) to make saner and shorter stack traces on resume
var current = this
var param = result
while (true) {
// Invoke "resume" debug probe on every resumed continuation, so that a debugging library infrastructure
// can precisely track what part of suspended callstack was already resumed
probeCoroutineResumed(current)
with(current) {
val completion = completion!! // fail fast when trying to resume continuation without completion
val outcome: Result<Any?> =
try {
// 执行协程体,可能会发生错误
val outcome = invokeSuspend(param)
if (outcome === COROUTINE_SUSPENDED) return
Result.success(outcome)
} catch (exception: Throwable) {
// 捕获错误并将其封装为Result
Result.failure(exception)
}
releaseIntercepted() // this state machine instance is terminating
if (completion is BaseContinuationImpl) {
// unrolling recursion via loop
current = completion
param = outcome
} else {
// top-level completion reached -- invoke and return
// 找到顶级的父协程,将错误交付给他,此时错误处理已经开始了
completion.resumeWith(outcome)
return
}
}
}
}
}
}
首先,我们先弄明白,谁属于BaseContinuationImpl,他是我们生成的SuspendLambda块。那么我们循环完毕找到的第一个不属于该类的协程是谁呢?他就是最上面的launch(当前也可能是其他的哈)-->StandaloneCoroutine--> AbstractCoroutine, 所以,真正的错误处理来到了AbstractCoroutine这里
AbstractCoroutine.resumeWith(result: Result<T>) {
val state = makeCompletingOnce(result.toState()) {
val finalState = tryMakeCompleting(state, proposedUpdate) {
......
// 为exception时必定走这里
return tryMakeCompletingSlowPath(state, proposedUpdate) {
......
// 先把子协程任务掉取消掉
return finalizeFinishingState(finishing, proposedUpdate) {
// Now handle the final exception---此时开始真正处理错误了
if (finalException != null) {
// 首先将错误扔给父类,询问其是否进行处理
// 如果父是Job,那么直接就取消掉了,已经handle完毕;如果为SupervisorJob,那么自己来处理了
val handled = cancelParent(finalException) || handleJobException(finalException) {
// 开始自己处理exception---别忘了我们是StandaloneCoroutine哈,此处用的就是他的处理
handleCoroutineException(context, exception) {
// Invoke an exception handler from the context if present
try {
// 找到上下文中的异常处理器,解决对应的内容哦
// 需要明确的一件事情是:由于CorroutineContext是存在加和的,所以,父协程一旦设置了异常处理器,子协程和其建立attach关系后,就会获取到其中的exceptionHandler用于异常处理了,这时候打印出来的内容就是父Handler的处理信息。当然,我们也可以给自身设置,这个有覆盖关系,会使用新的顶替掉父的
context[CoroutineExceptionHandler]?.let {
it.handleException(context, exception)
return
}
} catch (t: Throwable) {
handleCoroutineExceptionImpl(context, handlerException(exception, t))
return
}
// If a handler is not present in the context or an exception was thrown, fallback to the global handler
// 没有处理器,只能用最原始的去处理了
handleCoroutineExceptionImpl(context, exception) {
// use additional extension handlers
for (handler in handlers) {
try {
handler.handleException(context, exception)
} catch (t: Throwable) {
// Use thread's handler if custom handler failed to handle exception
val currentThread = Thread.currentThread()
currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, handlerException(exception, t))
}
}
// use thread's handler
val currentThread = Thread.currentThread()
// 这里使用当前线程的异常处理去做最后的捕获,现象就是崩掉了
currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception)
}
}
return true
}
if (handled) (finalState as CompletedExceptionally).makeHandled()
......
}
}
}
}
}
if (state === COMPLETING_WAITING_CHILDREN) return
afterResume(state)
}
上述部分就是整个异常的处理流程了。一旦产生了异常,他就会将其先进行传播(简单来说,子协程只是先想让父去解决掉这个问题,如果父Job为SupervisorJob,那么流转结束了,由其下的第一个子协程完成处理。否则会一直往上传递,去判断是否可以解决(也是流转给自己的父Job),直到遇到SupervisorJob或者一直到顶层了,那么就取消掉了流转中遇到的所有任务块(因为有一个模块已经出问题了),并由其下的第一个协程处理错误---除了CancellationException父类会帮忙处理掉)
更形象的说明,子协程出问题了,上报给自己的父协程:"我现在有问题,帮我解决"。Job拿到了,会帮忙处理掉这个问题--即他也一层层往上传递,让他的父Job帮他解决这个信息,同时,将同级子协程全部干掉;SupervisorJob拿到了,直接打回,子协程自己拿资源(上下文中的exceptionHandler)去解决,解决不掉,那么就是当前线程的uncaughtExceptionHandler兜底。
当然,这一切执行的前提条件,就是他们需要正常的关联到---即父子关系需要有链接。
下面的内容为具体执行取消时的逻辑,这里就会将整体的取消逻辑传递上去。
private fun notifyCancelling(list: NodeList, cause: Throwable) {
// first cancel our own children
onCancelling(cause) // 取消掉自身(cause可能为null,代表当前为正常执行结束;为 CancellationException,代表自己正常取消执行;其他错误,代表异常取消自身)
notifyHandlers<JobCancellingNode<*>>(list, cause) // 取消掉自身的子协程及其回调,如果为子协程,触发parentCancelled,内部还是cancelImpl,递归取消掉其自身及子协程
// then cancel parent 取消掉父协程,具体调用为parent.childCancelled(cause),parent是一个ChildHandle接口指向的对象,该接口最终被上面提到的ChildHandleNode类实现。
// childCancelled方法最终又会调用JobSupport类的childCancelled方法,如此递归,完成整体错误的取消。对于SupervisorJob,他改写了childCancelled方法,使得错误不再传递到父类(父类处理并中断传播,但是出错的协程及其子协程仍会被取消掉)
cancelParent(cause) // tentative cancellation -- does not matter if there is no parent
}
4. SupervisorJob和Job的区别(Supervisor --- 监督者、管理者)--体现在取消流程中
在上面的章节中,我们看到了两者之间的区别,SupervisorJob回绝了子协程请求的异常处理,具体的区分就体现在其内部的覆写方法中
private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
// 唯一区别就是覆写了这个方法,当子协程请求父协程进行异常处理的时候,这里就一口回绝了,子协程只能自身处理错误了
override fun childCancelled(cause: Throwable): Boolean = false
}
5. 错误的最终处理者--CoroutineExceptionHandler
CoroutineExceptionHandler是异常处理的最后手段(因为上层也会对异常进行处理,只有无法进行处理的内容,才会交由其处理---因此,在这里就保证了CancellationException无法被其触碰到,因为上层已经处理掉了)。这意味着当调用CoroutineExceptionHandler处理异常时,协程已经处于完成状态,不可再恢复执行。因此,如果需要在异常发生后继续执行,可以在协程中可能发生异常的位置使用try-catch代码块捕获异常(捕获处理了,就不会再上报过去了,这样是不会影响其正常执行下去的)。
当协程上下文中存在异常处理器,异常由其处理。而当协程上下文中没有异常处理器时,如果异常为取消异常,则会被忽略。如果异常不是取消异常,则会调用上下文中Job对象的cancel方法。如果上下文中Job对象不存在,则会调用JVM全局的CoroutineExceptionHandler对象来处理,并同时调用当前线程的uncaughtExceptionHandler处理。