Kotlin 协程之二:原理剖析

Kotlin 协程之二:原理剖析

  • 1.协程作用域(运行环境)-CoroutineScope
  • 2.协程任务执行环境-Dispatcher
    • (1)Dispatcher.Main
    • (2)Dispatcher.IO
      • 线程池-CoroutineScheduler
        • i.尽量使用当前线程
        • ii.双重队列
        • iii.抢占其他线程的任务
  • 3.协程任务-Coroutine
    • (1)AbstractCoroutine
    • (2)BaseContinuationImpl
      • i.invokeSuspend()
      • ii.COROUTINE_SUSPENDED
      • iii.completion.resumeWith(res)
    • (3)DispatchedContinuation
    • (4)总结
  • 4.协程挂起-suspend
    • (1)delay()
    • (2)async{}.await()
    • (3)withContext()
    • (4)自定义挂起
  • 5.协程异常
    • async{}.await()
  • 6.协程取消

系列文章:
Kotlin 协程之一:基础使用
Kotlin 协程之二:原理剖析
Kotlin 协程之三:Android中的应用

前面文章过后,我们知道了使用协程的基本步骤后,现在就来对照一些简单的demo和源码,逐层分析一下协程的实现原理吧!

1.协程作用域(运行环境)-CoroutineScope

协程作用域,也就是运行环境,顾名思义,会包含一个协程运行所需的各种参数,比如dispatcher派发器、interceptor拦截器、Job任务管理对象等。
Kotlin 协程之二:原理剖析_第1张图片

public interface CoroutineScope {
    //协程运行环境(上下文,如dispatcher)
    public val coroutineContext: CoroutineContext
}

每个scope需要一个上下文CoroutineContext对象,context主要是由几个对象组成:

  1. CoroutineDispatcher:执行协程任务的派发器,比如Dispatcher.IO、Dispatcher.Main等,事实上,这些dispatcher对象直接实现了CoroutineContext接口,成为一个context。

  2. ContinuationInterceptor:协程的拦截器,协程内部是通过多层代理模式(下面会讲)实现每一层的功能,其中就用到了拦截器实现其代理对象。

  3. Job:Job是协程任务的接口,里面定义了协程的状态字段、cancel方法、attach方法等;JobSupport类实现了Job的接口,并且提供了协程执行操作的接口,是处理一个协程任务的各种调用的集合;事实上每个协程都继承自JobSupport类,这样每个协程就有了被调用、改变状态的能力;每个协程的context是父协程的context以及自己本身的Job组成,所以每个协程是一个独立的Job对象进行运作,而CoroutineScope里的context,作为顶层context,他的Job是一个JobImpl对象。 Kotlin 协程之二:原理剖析_第2张图片

//创建scope时会新建JobImpl
public fun CoroutineScope(context: CoroutineContext): CoroutineScope = ContextScope(if (context[Job] != null) context else context + Job())
public fun Job(parent: Job? = null): CompletableJob = JobImpl(parent)
class JobImpl(parent: Job?) : JobSupport(true)
//每个协程是一个JobSupport,且context为父context+自身的Job
class AbstractCoroutine<in T>() : JobSupport(active){
	public final override val context: CoroutineContext = parentContext + this
}

CoroutineScope的实现简单概括:指定CoroutineContext对象创建CoroutineScope,用CoroutineScope启动协程任务,CoroutineContext的interceptor包装协程,然后通过CoroutineContext的dispatcher调度协程,执行时通过协程的Job(自身)进行调用。

2.协程任务执行环境-Dispatcher

首先,协程的dispatcher都继承自CoroutineDispatcher类,并且该类也继承自CoroutineContext,所以Dispatcher本身就是一个CoroutineContext。

public abstract class CoroutineDispatcher : AbstractCoroutineContextElement(ContinuationInterceptor)
public abstract class AbstractCoroutineContextElement(public override val key: Key<*>) : Element
public interface Element : CoroutineContext

这里我们主要看一下我们最常使用的Dispatcher.Main和Dispatcher.IO两个派发器。
Kotlin 协程之二:原理剖析_第3张图片

(1)Dispatcher.Main

Dispatcher.Main没有默认实现,依赖于各个平台的实现,如果没有引入android依赖包,则会抛异常提示,那么kotlin是怎么支持这种动态的类呢?

  1. 首先kotlin提供了一个工厂类接口,用来创建MainDispatcher

    public interface MainDispatcherFactory {
        fun createDispatcher(allFactories: List<MainDispatcherFactory>): MainCoroutineDispatcher
    }
    
  2. 然后再看反编译的源码

    public final <S> List<S> loadProviders$kotlinx_coroutines_core(@NotNull Class<S> paramClass, @NotNull ClassLoader paramClassLoader) {
      	//从apk的META-INF/services/文件夹下那类名
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("META-INF/services/");
        stringBuilder.append(paramClass.getName());
        Enumeration enumeration = paramClassLoader.getResources(stringBuilder.toString());
        ArrayList arrayList = Collections.list(enumeration);
        Iterable iterable = (Iterable)arrayList;
        Collection collection = (Collection)new ArrayList();
        for (URL uRL : iterable) {
          FastServiceLoader fastServiceLoader = INSTANCE;
          Intrinsics.checkExpressionValueIsNotNull(uRL, "it");
          CollectionsKt.addAll(collection, (Iterable)fastServiceLoader.parse(uRL));
        } 
        collection = CollectionsKt.toSet((Iterable)collection);
        iterable = (Iterable)collection;
        collection = (Collection)new ArrayList(CollectionsKt.collectionSizeOrDefault(iterable, 10));
      	//将类名解析为实例对象
        for (String str : iterable)
          collection.add(INSTANCE.getProviderInstance(str, paramClassLoader, paramClass)); 
        return (List)collection;
      }
    

    MainDispatcher的factory会从apk的META-INF/services/文件夹下获取。

  3. 再看编译生成的apk文件的该文件夹内容
    Kotlin 协程之二:原理剖析_第4张图片
    所以android的依赖包是通过向该文件注册类名实现的注册类,并且factory类为AndroidDispatcherFactory。

  4. 最后我们再来看下AndroidDispatcherFactory类

    internal class AndroidDispatcherFactory : MainDispatcherFactory {
        override fun createDispatcher(allFactories: List<MainDispatcherFactory>) = HandlerContext(Looper.getMainLooper().asHandler(async = true), "Main")
    }
    internal class HandlerContext private constructor(
        private val handler: Handler,
        private val name: String?,
        private val invokeImmediately: Boolean
    ) : HandlerDispatcher(), Delay {
        public constructor(
            handler: Handler,
            name: String? = null
        ) : this(handler, name, false)
    
      	//android中需要向主looper进行提交调度
        override fun isDispatchNeeded(context: CoroutineContext): Boolean {
            return !invokeImmediately || Looper.myLooper() != handler.looper
        }
    
      	//通过持有主线程looper的handler进行调度
        override fun dispatch(context: CoroutineContext, block: Runnable) {
            handler.post(block)
        }
      	...
    }
    

    很清楚,就是用持有主线程looper的handler进行任务的调度,确保任务会在主线程执行。

(2)Dispatcher.IO

internal object DefaultScheduler : ExperimentalCoroutineDispatcher() {
    val IO = blocking(systemProp(IO_PARALLELISM_PROPERTY_NAME, 64.coerceAtLeast(AVAILABLE_PROCESSORS)))
  	public fun blocking(parallelism: Int = BLOCKING_DEFAULT_PARALLELISM): CoroutineDispatcher {
        return LimitingDispatcher(this, parallelism, TaskMode.PROBABLY_BLOCKING)
    }
}

Dispatcher.IO是一个LimitingDispatcher实例,他可以控制同时并发任务数,默认为64个,即最多有64个任务同时在运行。

private class LimitingDispatcher(
    val dispatcher: ExperimentalCoroutineDispatcher,
    val parallelism: Int,
    override val taskMode: TaskMode
) : ExecutorCoroutineDispatcher()

而LimitingDispatcher内部真正调度任务的dispatcher是一个ExperimentalCoroutineDispatcher对象。

open class ExperimentalCoroutineDispatcher(
    private val corePoolSize: Int,
    private val maxPoolSize: Int,
    private val idleWorkerKeepAliveNs: Long,
    private val schedulerName: String = "CoroutineScheduler"
) : ExecutorCoroutineDispatcher() {
    constructor(
        corePoolSize: Int = CORE_POOL_SIZE,
        maxPoolSize: Int = MAX_POOL_SIZE,
        schedulerName: String = DEFAULT_SCHEDULER_NAME
    ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS, schedulerName)

    private var coroutineScheduler = createScheduler()

    override fun dispatch(context: CoroutineContext, block: Runnable): Unit =
        try {
            coroutineScheduler.dispatch(block)
        } catch (e: RejectedExecutionException) {
            DefaultExecutor.dispatch(context, block)
        }

    private fun createScheduler() = CoroutineScheduler(corePoolSize, maxPoolSize, idleWorkerKeepAliveNs, schedulerName)
}

我们看到,该dispatcher里面的真正的线程池,是CoroutineScheduler对象,而核心线程数和最大线程数,取决于可与CPU的数量。

internal val CORE_POOL_SIZE = systemProp(
    "kotlinx.coroutines.scheduler.core.pool.size",
    AVAILABLE_PROCESSORS.coerceAtLeast(2), // !!! at least two here
    minValue = CoroutineScheduler.MIN_SUPPORTED_POOL_SIZE
)
internal val AVAILABLE_PROCESSORS = Runtime.getRuntime().availableProcessors()

线程池-CoroutineScheduler

这里我们挑几个小细节看一下和通常的线程池有何不一样。

i.尽量使用当前线程

private fun submitToLocalQueue(task: Task, fair: Boolean): Int {
        val worker = currentWorker() ?: return NOT_ADDED
  			...
        worker.localQueue.add(task, globalQueue)
  			...
}
private fun currentWorker(): Worker? = (Thread.currentThread() as? Worker)?.takeIf { it.scheduler == this }

如果当前线程是Dispatcher.IO开启的工作线程,那么任务优先交由该线程的任务队列,等待处理。

ii.双重队列

fun dispatch(block: Runnable, taskContext: TaskContext = NonBlockingContext, fair: Boolean = false) {
  ...
  when (submitToLocalQueue(task, fair)) {
    ADDED -> return
    NOT_ADDED -> {
      //本地队列已满,放入全局队列,所有线程可取
      globalQueue.addLast(task)
    }
    else -> requestCpuWorker() // ask for help
  }
}

如果工作线程本地队列无限大,一味的放入本地队列的话,可能会造成单一线程工作,效率极低,于是每个工作线程有固定大小的queue,满了之后,会放到全局queue中,等待任意空闲工作线程执行。

iii.抢占其他线程的任务

//工作线程Worker类
override fun run() {
  while (!isTerminated && state != WorkerState.TERMINATED) {
    val task = findTask()
    ...
  }
  ...
}
private fun findTaskWithCpuPermit(): Task? {
	...
  //从本地queue获取任务
  localQueue.poll()?.let { return it }
  //从全局queue获取任务
  if (!globalFirst) globalQueue.removeFirstOrNull()?.let { return it }
  //抢占其他线程任务
  return trySteal()
}
private fun trySteal(): Task? {
  ...
  //随机一个工作线程
  if (stealIndex == 0) stealIndex = nextInt(created)
  ...
  val worker = workers[stealIndex]
  if (worker !== null && worker !== this) {
    //将其queue里的任务放到自己queue中
    if (localQueue.trySteal(worker.localQueue, globalQueue)) {
      return localQueue.poll()
    }
  }
  return null
}

如果一个工作线程的本地queue和全局queue都没有任务了,但是其他线程的queue还有任务,此时让其空闲,一是没有充分利用线程提升工作效率,而是线程的空闲状态切换需要开销,所以此时会尝试从任一工作线程的queue中取出任务,放入自己的queue中执行。

以上三点的相互配合,可以充分利用线程资源,避免过多线程的使用及开销,也保证了多任务时的工作效率。

3.协程任务-Coroutine

了解了scope和dispatcher后,接下来我们就要创建协程任务了,这里就拿最简单的CoroutineScope(Dispatchers.IO).launch{ }例子来说。

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}
  1. 这里的start参数,是协程的开启方式,比如默认的DEFAULT是直接执行协程任务,还有LAZY模式,直到我们手动调用协程的start()方法才会执行,相当于一个懒加载的功能。

  2. 这里的newCoroutineContext()方法,会合并当前scope的context和传入的context生成一个新的context,作为新协程的context,这里需要明确的是:

    1. context上面说过是scope的上下文执行环境,那协程为什么需要context呢?因为每个协程也实现了CoroutineScope,是一个scope,这也是为什么我们可以在协程内部再建协程!

    2. newCoroutineContext()的合并原则是,两者不同的属性保留,相同的属性则替换为传入的context的属性(即覆盖父context),而如果一方是Empty类型的Context,那么就完全取决于另一方,所以通常情况下,我们在一个scope内部创建的协程,context包括其中的dispatcher等都是来自于scope,除非我们想新建一个在其他dispatcher执行的协程,可以传入context,如:

      CoroutineScope(Dispatchers.IO).launch {
        launch(Dispatchers.Main){ }
      }
      

      此时内部的launch协程的context,就是Dispatcher.Main了。

  3. 这里的block参数,其实就是我们的协程代码块,这个部分稍后会细说。

  4. 有了这些参数,然后我们会创建一个StandaloneCoroutine对象,这就是一个标准协程对象,这里需要解释的是:

    1. 协程对象都继承自AbstractCoroutine类,代表一个协程。

    2. 协程对象都实现了CoroutineScope接口,是一个scope(上面提到过)。

    3. 协程是将异步任务(一大块代码)分片段执行(执行一段,挂起,再执行一段,再挂起),同一个协程会多次被执行,每次执行一段代码,所以协程对象也实现了Continuation接口,作为一个可执行的代码片段。

Kotlin 协程之二:原理剖析_第5张图片
然后我们接着来看AbstractCoroutine的start方法。

public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
  //初始化父子关系
  initParentJob()
  //调用CoroutineStart,开启协程
  start(block, receiver, this)
}
//不同STRAT模式调用不同方法
public enum class CoroutineStart {
  public operator fun <R, T> invoke(block: suspend R.() -> T, receiver: R, completion: Continuation<T>) =
        when (this) {
            CoroutineStart.DEFAULT -> block.startCoroutineCancellable(receiver, completion)
            CoroutineStart.ATOMIC -> block.startCoroutine(receiver, completion)
            CoroutineStart.UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
            CoroutineStart.LAZY -> Unit // will start lazily
        }
}

这里我们会看到,最终会根据不同的START模式来启动协程,参数completion就是我们上一步创建的AbstractCoroutine协程对象。

比如LAZY就不启动,而是需要我们后续手动调用start(),这里我们来看DEFAULT模式。

internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(receiver: R, completion: Continuation<T>) =
    runSafely(completion) {
        createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellable(Unit)
    }

这段代码就是真正启动协程的逻辑,也是上面提到的,协程是通过多层代理模式实现各层功能的,下面我们逐层分析一下,这也是协程实现的重点!

(1)AbstractCoroutine

上面创建的AbstractCoroutine是第一层协程对象,他的主要功能是提供协程状态改变的逻辑。

public final override fun resumeWith(result: Result<T>) {
  makeCompletingOnce(result.toState(), defaultResumeMode)
}

resumeWith()是恢复协程的重要方法,也是入口,其中调用的逻辑都是在父类JobSupport中实现。

createCoroutineUnintercepted()方法中传入的completion就是第一层的AbstractCoroutine对象。

(2)BaseContinuationImpl

AbstractCoroutine是处理协程状态的,那协程任务的逻辑代码呢?是交由第二层BaseContinuationImpl处理的。

internal abstract class BaseContinuationImpl(
    public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
    public final override fun resumeWith(result: Result<Any?>) {
        ...
        val completion = completion!!
        val outcome: Result<Any?> =
        try {
          val outcome = invokeSuspend(param)
          if (outcome === COROUTINE_SUSPENDED) return
          Result.success(outcome)
        } catch (exception: Throwable) {
          Result.failure(exception)
        }
      	...
        completion.resumeWith(outcome)
      	return
    }

    protected abstract fun invokeSuspend(result: Result<Any?>): Any?
}

首先,该对象也可以分段执行代码逻辑的,其实现了Continuation接口,resumeWith()就是恢复协程调用的方法,它里面的逻辑,是调用invokeSuspend()方法,这个方法内部就是我们自己写的协程任务的代码,然后有两种处理结果:

  1. 如果此次调用返回的是COROUTINE_SUSPENDED,那么直接返回。

  2. 如果此次调用结果返回的正常结果或者出现异常,则将结果/异常对象包装为Result,返回调用completion的resumeWith()方法并传入Result。

那么这里就有几个问题了:

i.invokeSuspend()

我们自己写的代码,怎么会跑到了该类的invokeSuspend()方法里了呢?

这是kotlin的协程,在kotlin编译器的编译源代码时候,自动完成将我们的代码块,转换为一个BaseContinuationImpl子类的。

比如如下代码块:

CoroutineScope(Dispatchers.IO).launch {
  log("launch-start")
  delay(2000)
  log("launch-end")
}

launch代码块,在经过编译器转换后生成的类如下:

final class TestCoroutines$t$1$1$1 extends SuspendLambda implements Function2<CoroutineScope, Continuation<? super Unit>, Object> {
  
  int label = 0;
  
  TestCoroutines$t$1$1$1(Continuation paramContinuation) { super(2, paramContinuation); }
  
  public final Object invoke(Object paramObject1, Object paramObject2) { return ((null)create(paramObject1, (Continuation)paramObject2)).invokeSuspend(Unit.INSTANCE); }
  
  @Nullable
  public final Object invokeSuspend(@NotNull Object paramObject) {
    Object object = IntrinsicsKt.getCOROUTINE_SUSPENDED();
    switch (this.label) {
      default:
        throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
      case 1:
        object = (CoroutineScope)this.L$0;
        ResultKt.throwOnFailure(paramObject);
        TestCoroutines.access$log(TestCoroutines.INSTANCE, "launch-end");
        return Unit.INSTANCE;
      case 0:
        break;
    } 
    ResultKt.throwOnFailure(paramObject);
    paramObject = this.p$;
    TestCoroutines.access$log(TestCoroutines.INSTANCE, "launch-start");
    this.L$0 = paramObject;
    this.label = 1;
    if (DelayKt.delay(2000L, this) == object)
      return object; 
    TestCoroutines.access$log(TestCoroutines.INSTANCE, "launch-end");
    return Unit.INSTANCE;
  }
}

于是乎,终于看到了我们写的代码在哪了,该类是编译器生成的,继承了SuspendLambda(其继承了BaseContinuationImpl),也实现了Function2接口(和我们launch方法接受的block参数类型一致),所以说BaseContinuationImpl是第二层协程对象,主要负责通过调用invokeSuspend()执行我们的任务代码。

那么是如何将我们的代码分段执行的呢?通过仔细观察重写的invokeSuspend()方法实现,我们发现了是使用状态机模式来实现的:

  1. int变量label,作为状态变量,初始值为0.

  2. 第一次调用invokeSuspend()时,走到case 0,执行了"launch-start"的log输出,然后执行delay(2000)语句,这是个挂起携程的方法,DelayKt.delay(2000L, this)刚开始肯定返回IntrinsicsKt.getCOROUTINE_SUSPENDED(),所以直接return了,这时,这个协程任务这次的调用就结束了,不会阻塞线程,当然不要忘了把状态label改为1,等待下次执行。

  3. 等到2秒后,delay协程任务完成了,会自动通知到被挂起的协程,进行resumeWith()恢复,然后执行第二次invokeSuspend(),此时走到case 1,执行了"launch-end"的log输出,然后结束,至此这个协程任务执行完毕。

ii.COROUTINE_SUSPENDED

第二个问题是,协程如何知道要被挂起?其实我们已经说到了,就是在invokeSuspend()方法里,如果遇上类似delay()这种挂起协程的方法,那么会直接return返回COROUTINE_SUSPENDED值,而在BaseContinuationImpl的resumeWith()中,我们看到,如果返回的是这个值,那么直接return此次执行,并不会调用completion的resumeWith(),说明协程没有结束,已被挂起,等待下次执行。

iii.completion.resumeWith(res)

如果协程任务全部结束,或像上面说的那样有了异常,会调用了completion.resumeWith()结束协程,那么这个completion是谁呢?其实就是第一层的AbstractCoroutine协程对象,因为他才是用来改变协程状态的对象。那么他俩是如何关联起来的呢?我们来看start时的createCoroutineUnintercepted()方法。

public static final <R, T> Continuation<Unit> createCoroutineUnintercepted(@NotNull Function2<? super R, ? super Continuation<? super T>, ? extends Object> paramFunction2, R paramR, @NotNull Continuation<? super T> paramContinuation) {
  ...
  if (paramFunction2 instanceof BaseContinuationImpl)
  	return ((BaseContinuationImpl)paramFunction2).create(paramR, paramContinuation); 
  ...
}
internal abstract class BaseContinuationImpl(
    public val completion: Continuation<Any?>?
)

可以看到,其实就是第一层的协程对象,让第二层的协程对象代理了。

(3)DispatchedContinuation

我们来看下一步intercepted()方法,这里就简写了。

public static final <T> Continuation<T> intercepted(@NotNull Continuation<? super T> paramContinuation) {
  ...
  ContinuationImpl continuationImpl = (ContinuationImpl)paramContinuation;
  return continuationImpl.intercepted();
}

我们的代码块生成的类,其实都是继承了SuspendLambda类,该类又继承了ContinuationImpl类,该类又继承了BaseContinuationImpl类,所以这里的强转没有问题,然后来看下ContinuationImpl的intercepted()方法。

public fun intercepted(): Continuation<Any?> =
        intercepted
            ?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
                .also { intercepted = it }

方法意思是,使用context里的ContinuationInterceptor属性的interceptContinuation()方法进行拦截,而我们一般使用的Dispatcher.IO或Dispatcher.Main,都是继承了CoroutineDispatcher类,来看一下他的实现。

public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> = DispatchedContinuation(this, continuation)

直接返回了一个DispatchedContinuation,并传入了第二层协程对象进行代理,这就是我们的第三层协程对象,他的作用顾名思义,就是用来调度协程的。

override fun resumeWith(result: Result<T>) {
  val context = continuation.context
  val state = result.toState()
  if (dispatcher.isDispatchNeeded(context)) {
    _state = state
    resumeMode = MODE_ATOMIC_DEFAULT
    dispatcher.dispatch(context, this)
  }...
}

基本的调度逻辑就是使用dispatcher进行调度,dispatcher在上面已经说过了,说白了就是线程池调度任务罢了,而这每个DispatchedContinuation都继承了DispatchedTask,是一个实现了Runnable的对象,可以用于线程的执行。

public final override fun run() {
  try {
    val delegate = delegate as DispatchedContinuation<T>
    //第二层协程对象BaseContinuationImpl
    val continuation = delegate.continuation
    ...
    withCoroutineContext(context, delegate.countOrElement) {
      val exception = getExceptionalResult(state)
      //调用第二层对象的恢复逻辑(正常或异常状态)
      if (exception != null) continuation.resumeWithStackTrace(exception)
      else continuation.resume(getSuccessfulResult(state))
      ...
    }
  }...
}

这也是代理模式的基本使用。最后再来看一下resumeCancellable()方法。

public static final void resumeCancellable(@NotNull Continuation $this$resumeCancellable, Object value) {
  if ($this$resumeCancellable instanceof DispatchedContinuation) {
    DispatchedContinuation this_$iv = (DispatchedContinuation)$this$resumeCancellable;
    $i$f$resumeCancellable = false;
    if (this_$iv.dispatcher.isDispatchNeeded(this_$iv.getContext())) {
      this_$iv._state = value;
      this_$iv.resumeMode = 1;
      //直接派发自己就完事了!
      this_$iv.dispatcher.dispatch(this_$iv.getContext(), (Runnable)this_$iv);
    } else ...
  }...
}

直接调用了第三层协程的对象派发逻辑,进行派发。

(4)总结

一下子讲了三层,来捋一捋。
Kotlin 协程之二:原理剖析_第6张图片
Kotlin 协程之二:原理剖析_第7张图片
说白了协程的实现原理就是:

  1. 第一层对象用来处理协程状态。

  2. 第二层对象由编译器将我们的代码转换而成,利用状态机实现代码分段执行和协程挂起,并代理第一层对象。

  3. 第三层对象负责使用dispatcher进行调度任务,并代理第二层对象。

  4. 每次执行时,调用第三层对象的派发逻辑执行任务,每次执行的是第二层对象的代码逻辑方法,执行到协程挂起后,改变状态,并返回,等待下一次的执行,下一次执行重复iv。

4.协程挂起-suspend

协程的基本执行我们清楚了,再来细说一下协程的挂起,刚刚讲到过,协程的所谓挂起,其实就是在某个代码处,直接return协程任务,等待下次被恢复,这样可以避免像Thread.sleep这种方法长时间阻塞线程,那么我们如何挂起一个协程,也就是如何在某个代码处返回COROUTINE_SUSPENDED呢?又是如何在某个时机恢复被挂起的协程呢?我们来看几个实际案例。

(1)delay()

delay()方法已经说了好多次了,我们现在来实际分析一下他的实现。

public suspend fun delay(timeMillis: Long) {
    return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
        cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
    }
}
public suspend inline fun <T> suspendCancellableCoroutine(
    crossinline block: (CancellableContinuation<T>) -> Unit
): T = suspendCoroutineUninterceptedOrReturn { uCont ->
        val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
        block(cancellable)
        cancellable.getResult()
    }

首先我们可以看到,调用了suspendCoroutineUninterceptedOrReturn()方法,这个方法是什么用呢?我们看名字也能看出来,这就是用来挂起协程或直接返回结果(协程可能已经运行完毕)的方法。

private static final <T> Object suspendCoroutineUninterceptedOrReturn(Function1<? super Continuation<? super T>, ? extends Object> paramFunction1, Continuation<? super T> paramContinuation) {
  return paramFunction1.invoke(CoroutineIntrinsics.normalizeContinuation(paramContinuation));
}

我们看到,直接调用block,并返回block的结果。那block做了什么呢?

  1. 将当前协程(uCont)包装为一个新的CancellableContinuationImpl对象,并代理了当前协程(DispatchedContinuation)对象。

  2. 在将这个对象回调到delay()的block里,做了什么呢?调用了context的delay队列的scheduleResumeAfterDelay()方法。

    override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) = schedule(DelayedResumeTask(timeMillis, continuation))
    private inner class DelayedResumeTask(
            timeMillis: Long,
            private val cont: CancellableContinuation<Unit>
    ) : DelayedTask(timeMillis) {
      override fun run() {
        with(cont) { resumeUndispatched(Unit) }
      }
    }
    

    schedule就是像一个队列里放入了一个任务,等到指定时间后,就会运行这个任务,运行的逻辑很简单,就是调用传入的cont,即当时被挂起的协程的resume()方法,后面的事就都知道了吧!

  3. 此次调用delay()方法,返回的是包装的当前协程的result。

    internal fun getResult(): Any? {
      if (trySuspend()) return COROUTINE_SUSPENDED
      ...
      return getSuccessfulResult(state)
    }
    

    如果协程还没有执行结束,则会返回COROUTINE_SUSPENDED,此时外部协程,就会像上面说的,协程直接return了,被挂起了;而如果协程已经运行完毕,则把结果直接返回了,协程就不会挂起了。

    所以说,协程并不是一定会被挂起,这点很重要。

(2)async{}.await()

我们再来看一个async式协程的await()方法。我们只看关键

private suspend fun awaitSuspend(): Any? = suspendCoroutineUninterceptedOrReturn { uCont ->
        val cont = AwaitContinuation(uCont.intercepted(), this)
        cont.disposeOnCancellation(invokeOnCompletion(ResumeAwaitOnCompletion(this, cont).asHandler))
        cont.getResult()
    }

其实和delay几乎是一样的,只有后续的恢复协程的方式不一样。invokeOnCompletion()是将一个ResumeAwaitOnCompletion对象,作为node保存到当前协程,即要被挂起的外部协程中,其中保存了当前外部协程的封装对象AwaitContinuation,用于后续恢复。

当async协程执行完毕,调用其第一层对象AbstractCoroutine的resumeWith()方法时,会调到notifyHandlers方法。

private inline fun <reified T: JobNode<*>> notifyHandlers(list: NodeList, cause: Throwable?) {
  list.forEach<T> { node ->
                   try {
                     node.invoke(cause)
                   }...
                  }
}

该list就是node的list,此时会调用ResumeAwaitOnCompletion。

private class ResumeAwaitOnCompletion<T>(
    job: JobSupport,
    private val continuation: CancellableContinuationImpl<T>
) : JobNode<JobSupport>(job) {
    override fun invoke(cause: Throwable?) {
        ...
	      continuation.resume(state.unboxState() as T)
    }
}

这个continuation,就是需要被恢复的协程,后面的事,大家也知道了。

(3)withContext()

delay()和await()其实差不多,这里再说一种withContext(),他的不一样是:在withContext()协程的所有内部协程(包括其子协程)都结束后,才会恢复挂起的外部协程,这是怎么做的呢?

首先,withContext()的创建过程。

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T = suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
    ...
    val coroutine = DispatchedCoroutine(newContext, uCont)
    coroutine.initParentJob()
    block.startCoroutineCancellable(coroutine, coroutine)
    coroutine.getResult()
}

和之前说的挂起协程方式一模一样,这里需要注意的是,创建的协程是DispatchedCoroutine类型,持有了待恢复的协程。

当withContext()协程的代码块执行完成,调用resumeWith(),会调用到tryMakeCompletingSlowPath()方法。

private fun tryMakeCompletingSlowPath(state: Incomplete, proposedUpdate: Any?, mode: Int): Int {
  ...
  // now wait for children
  val child = firstChild(state)
  if (child != null && tryWaitForChild(finishing, child, proposedUpdate))
  	return COMPLETING_WAITING_CHILDREN
  // otherwise -- we have not children left (all were already cancelled?)
  if (tryFinalizeFinishingState(finishing, proposedUpdate, mode))
  	return COMPLETING_COMPLETED
  ...
}
private tailrec fun tryWaitForChild(state: Finishing, child: ChildHandleNode, proposedUpdate: Any?): Boolean {
  val handle = child.childJob.invokeOnCompletion(
    invokeImmediately = false,
    handler = ChildCompletion(this, state, child, proposedUpdate).asHandler
  )
  if (handle !== NonDisposableHandle) return true // child is not complete and we've started waiting for it
  val nextChild = child.nextChild() ?: return false
  return tryWaitForChild(state, nextChild, proposedUpdate)
}

我们看到,在结束一个协程时,会判断其有没有子协程还没有完成(子协程与父协程的持有关系在创建协程时就已完成),如果有未完成的子协程,会创建一个ChildCompletion对象并持有withContext的协程,通过invokeOnCompletion()放入子协程的node list中。

当子协程完成时,按照上面说的,会执行notifyHandlers(),通知所有的node。

private class ChildCompletion(...) : JobNode<Job>(child.childJob) {
  override fun invoke(cause: Throwable?) {
    parent.continueCompleting(state, child, proposedUpdate)
  }
}
private fun continueCompleting(state: Finishing, lastChild: ChildHandleNode, proposedUpdate: Any?) {
  val waitChild = lastChild.nextChild()
  // try wait for next child
  if (waitChild != null && tryWaitForChild(state, waitChild, proposedUpdate)) return // waiting for next child
  // no more children to wait -- try update state
  if (tryFinalizeFinishingState(state, proposedUpdate, MODE_ATOMIC_DEFAULT)) return
}

node里会通知withContext的协程,继续判断是否有子协程未完成,如果都完成了,则执行tryFinalizeFinishingState()方法,最终执行到afterCompletionInternal()方法,而withContext创建的DispatchedCoroutine的该方法为:

override fun afterCompletionInternal(state: Any?, mode: Int) {
  if (state is CompletedExceptionally) {
    val exception = if (mode == MODE_IGNORE) state.cause else recoverStackTrace(state.cause, uCont)
    uCont.resumeUninterceptedWithExceptionMode(exception, mode)
  } else {
    uCont.resumeUninterceptedMode(state as T, mode)
  }
}

直接恢复被挂起的外部协程。

综上所述:

  1. 协程状态的最终完成,需要等到内部所有协程完成;但是仅限于状态的完成,不会影响协程任务本身代码块的执行完成。

  2. 诸如withContext()、coroutineScope()这类的协程,会挂起外部协程,直到内部所有协程完成才会恢复继续向下执行。

(4)自定义挂起

说了这么多,我们自己如何定义协程的挂起呢?需要通过两个kotlin提供的功能来实现:

  1. suspendCoroutine()相关的API,挂起协程,并在block里拿到待恢复的协程,在合适的时机调用resume进行恢复。

  2. suspend关键字,表示该方法是可挂起的方法,约束该方法只能在协程内部或者suspend方法内部调用。

    private suspend fun getData() = suspendCoroutine<String> {
      it.resume("result")
    }
    CoroutineScope(Dispatchers.IO).launch { val data = getData() }
    

Kotlin 协程之二:原理剖析_第8张图片

5.协程异常

下面来看异常部分,如果在invokeSuspend()方法中,我们的代码出现了异常,会怎么办呢?

val completion = completion!!
val outcome: Result<Any?> =
try {
  val outcome = invokeSuspend(param)
  if (outcome === COROUTINE_SUSPENDED) return
  Result.success(outcome)
} catch (exception: Throwable) {
  Result.failure(exception)
}
...
completion.resumeWith(outcome)

可以看到,将异常包装到Result中,还是调用resumeWith()方法,最终调用到tryMakeCompletingSlowPath()方法。

private fun tryMakeCompletingSlowPath(state: Incomplete, proposedUpdate: Any?, mode: Int): Int {
  ...
  notifyRootCause?.let { notifyCancelling(list, it) }
  ...
  if (tryFinalizeFinishingState(finishing, proposedUpdate, mode))
  	return COMPLETING_COMPLETED
	...
}
//1.cancel掉parent和children
private fun notifyCancelling(list: NodeList, cause: Throwable) {
  onCancelling(cause)
  notifyHandlers<JobCancellingNode<*>>(list, cause)
  cancelParent(cause)
}
//2.尝试处理exception
private fun tryFinalizeFinishingState(state: Finishing, proposedUpdate: Any?, mode: Int): Boolean {
  ...
  if (finalException != null) {
    val handled = cancelParent(finalException) || handleJobException(finalException)
    if (handled) (finalState as CompletedExceptionally).makeHandled()
  }
  ...
  //3.逐层向上通知结束
  completeStateFinalization(state, finalState, mode)
  return true
}

第一步,会将父协程和子协程cancel掉,第二步自己尝试处理异常,第三步逐层向上通知结束,并传递exception。

这里主要是第二步,cancelParent()方法取消父协程,一般都是返回true的,直到根协程会返回false,此时需要执行handleJobException()方法处理,也就是说只有根协程才会尝试处理异常。

protected open fun handleJobException(exception: Throwable): Boolean = false
private open class StandaloneCoroutine(...) : AbstractCoroutine<Unit>(parentContext, active) {
    override fun handleJobException(exception: Throwable): Boolean {
        handleCoroutineException(context, exception)
        return true
    }
}

这里发现个问题,协程基类里直接返回false,而StandaloneCoroutine(launch式协程)才会重写进行处理,难道除了launch协程,比如async协程作为根协程的话,就不处理异常了么?确实是这样的,比如下面这个代码:

CoroutineScope(Dispatchers.IO).async {
  launch { throw IllegalStateException("erroe") }
}

最终将没有异常抛出,一切正常。。。是不是很坑。

那StandaloneCoroutine协程是怎么处理的呢?

public fun handleCoroutineException(context: CoroutineContext, exception: Throwable) {
    ...
  	context[CoroutineExceptionHandler]?.let {it.handleException(context, exception)}
}

context的CoroutineExceptionHandler是什么呢?和上面说的Dispatcher.Main的来源一样,是根据在apk的META-INF下声明的类名拿到的类实例。
Kotlin 协程之二:原理剖析_第9张图片

internal class AndroidExceptionPreHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler, Function0<Method?> {

    override fun handleException(context: CoroutineContext, exception: Throwable) {
        val thread = Thread.currentThread()
        if (Build.VERSION.SDK_INT >= 28) {
            thread.uncaughtExceptionHandler.uncaughtException(thread, exception)
        } else {
            (preHandler?.invoke(null) as? Thread.UncaughtExceptionHandler)
                ?.uncaughtException(thread, exception)
        }
    }
}

处理很简单,就是交由线程的UncaughtExceptionHandler处理,也就是和正常的处理一直,抛出了异常,停止了进程。这段代码会正常抛出异常:

CoroutineScope(Dispatchers.IO).launch { launch { throw IllegalStateException("error") } }

async{}.await()

这里要单独说一下async协程的await方法的异常处理,当async协程代码块内部产生异常:

CoroutineScope(Dispatchers.IO).async {
  val job = async { throw IllegalStateException("error") }
  try {
    val res = job.await()
  } catch (e: Exception) {
  }
}

在恢复外部协程的时候,会将异常当做参数,即await()的结果传递回来,如果判断是异常类型,则会直接throw出去,所以如果此时我们用try-catch捕获await,是可以捕获到异常的,而await处理异常是编译器生成代码时完成的:

public final Object invokeSuspend(@NotNull Object paramObject) {
  case 0:
    ...
  	//挂起
    if (deferred.await(this) == coroutine_suspended) {
      return coroutine_suspended;
    }
    break;
  case 1:
  	//恢复后如果结果是异常则直接throw,此处result就是paramObject,是await()的结果
    if (result instanceof Failure) {
      throw ((Failure) result).exception;
    }
    break;
}

但是这里需要注意的是,如果根协程不是async协程,而是launch协程的话,还是会直接抛出异常的,因为在恢复外部协程之前就已经被launch协程处理了,这点很重要也很坑。。。

总结一下:

  1. 协程抛出异常,会取消父协程和子协程。

  2. 异常会逐步传递到根协程进行处理,async协程不作处理,launch协程正常抛出异常。

  3. async协程的await方法可以进行捕获异常,但是前提是根协程不能处理异常。

Kotlin 协程之二:原理剖析_第10张图片

6.协程取消

最后,再来看协程的取消,我们使用协程的cancel()(Job接口提供的方法)来取消一个协程。

public override fun cancel(cause: CancellationException?) { cancelInternal(cause) }

我们发现cancel方法可以传入一个CancellationException对象,用于描述取消的情况信息,而这个对象是一个Exception异常对象,接着看里面执行到的makeCancelling()方法。

private fun makeCancelling(cause: Any?): Boolean {
  when (state) {
    ...
    is Incomplete -> {
      val causeException = causeExceptionCache ?: createCauseException(cause).also { causeExceptionCache = it }
      if (state.isActive) {
        if (tryMakeCancelling(state, causeException)) return true
      }...
    }
  }
}
private fun tryMakeCancelling(state: Incomplete, rootCause: Throwable): Boolean {
  val cancelling = Finishing(list, false, rootCause)
  if (!_state.compareAndSet(state, cancelling)) return false
  notifyCancelling(list, rootCause)
  return true
}
private fun notifyCancelling(list: NodeList, cause: Throwable) {
  onCancelling(cause)
  notifyHandlers<JobCancellingNode<*>>(list, cause)
  cancelParent(cause)
}

首先将协程自身状态改为取消结束状态,然后通知取消父协程和子协程,这逻辑看上去和处理异常一样,但最后当然不是抛出异常的,我们接着看。

//子协程取消
private inline fun <reified T: JobNode<*>> notifyHandlers(list: NodeList, cause: Throwable?) {
  node.invoke(cause)
}
override fun invoke(cause: Throwable?) = childJob.parentCancelled(job)
public final override fun parentCancelled(parentJob: ParentJob) { cancelImpl(parentJob) }
internal fun cancelImpl(cause: Any?): Boolean { return makeCancelling(cause) }//正常取消
//父协程取消
private fun cancelParent(cause: Throwable): Boolean {
  val isCancellation = cause is CancellationException
  return parent.childCancelled(cause) || isCancellation//cancel()时返回一定为true,就不会调用handleJobException处理异常了
}
override fun childCancelled(cause: Throwable): Boolean = job.childCancelled(cause)
public open fun childCancelled(cause: Throwable): Boolean {
  if (cause is CancellationException) return true//cancel()时直接return,不会取消协程
  return cancelImpl(cause) && handlesException
}

我们看到,子协程会正常取消掉,而父协程由于异常是CancellationException类型,所以不会处理异常以及取消协程了。

总结一下:

  1. cancel()其实和异常处理逻辑一样,只是异常类型被特殊化处理,避免直接抛出异常和取消父协程了。

  2. 取消协程会取消所有子协程,而父协程不会被取消。

  3. 这里我们看到,取消协程,只是状态的变化(AbstractCoroutine层对象),而BaseContinuationImpl层的代码块逻辑执行没有影响,所以,取消协程不会取消代码块逻辑的运行,这点非常非常重要,对于代码块逻辑,可以通过协程的isActive状态变量来判断是否应该继续执行。

  4. isActive判断,在kotlin自带的一些API,如delay、async{}.await()里已经实现,这些方法我们不用自己处理。

    val job = launch {
      while (isActive) {
        log("launch")
      }
    }
    delay(1000)
    job.cancel()
    

你可能感兴趣的:(原理解析,kotlin相关,android相关)