上一篇文章Retrofit源码解析中,提到对于协程的一些个人理解,它并不是什么黑魔法,它只是将异步所需要的线程调度和传递回调封装或者自动生成。
将回调封装成Continuation
,调用它的 resume
或者 resumeWithException
来返回结果或者抛出异常,跟我们普通的回调并没有区别。
将调度封装成调度器,调度器的本质是一个协程拦截器,它拦截的对象就是Continuation
,进而在其中实现回调的调度。
本文重点就是分析协程中的调度器是怎样工作的。首先从它的基类协程上下文、协程拦截器开始看。
1. 协程上下文
协程上下文CoroutineContext
是一个接口,定义了一种数据结构,它的直接子类有三个Element
、EmptyCoroutineContext
、CombinedContext
。
public interface CoroutineContext {
public operator fun <E : Element> get(key: Key<E>): E?
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
public operator fun plus(context: CoroutineContext): CoroutineContext = ...
public fun minusKey(key: Key<*>): CoroutineContext
}
CoroutineContext
定义了四个方法:
CoroutineContext | 作用 |
---|---|
get(key: Key): E? | 通过Key查找Element |
fold( R, (R, Element) -> R): R | 遍历所有Element |
plus(CoroutineContext): CoroutineContext | 两个CoroutineContext相加组合 |
minusKey(key: Key<*>): CoroutineContext | 移除Key对应的Element |
看到这几个熟悉的方法,可以说CoroutineContext
是一个以Key
为索引的Element
集合。
1.1. Element
Element
本身也继承自CoroutineContext
,Element
也有这些集合的接口,不过它实现的是只有一个元素的集合。
1.2. EmptyCoroutineContext
EmptyCoroutineContext
是一个静态类,从名字就可以看出,它是一个空实现。
1.3. CombinedContext
CombinedContext
是一个单向链表实现,与平常的链表存储下一个元素不同,CombinedContext
存储的是上一个元素。
get()
方法查找从当前元素向上查找。
fold()
方法遍历的时候是最后再遍历当前元素,先递归调用处理前面的元素,最后再处理自身。
1.4. ContinuationInterceptor
CoroutineContext.Element
有一个主要的实现--拦截器ContinuationInterceptor
。
新增的interceptContinuation
接口,返回包装原始回调continuation
的一个新的continuation
,拦截所有的回调操作。
再回头看plus
的实现。
plus
的对两个CoroutineContext
进行了合并,合并的过程是将被合并的CoroutineContext
中一个个单独的element
,添加到当前CoroutineContext
中。
添加的过程中,移除掉同一个相同Key
的值,并且将拦截器放到了最后一项。
从这里可以看出CoroutineContext
的两个特点,同一种类型的CoroutineContext
在列表中只能存在一个,拦截器在列表的最后一项。又因为CombinedContext
结构的特殊性,他存储的是上一个元素的指针,所以最后一项也就是第一项。
2. 调度器
回到CoroutineScope.launch()
方法,CoroutineContext
的默认值是EmptyCoroutineContext
,也就是之前说的空实现。在函数体中,调用CoroutineScope.newCoroutineContext()
对传入的context
进行了封装。
封装的过程中,当未指定其他调度程序或ContinuationInterceptor
时,添加Dispatchers.Default
。
这里的Dispatchers.Default
就是一个调度器的默认实现,调度器的作用就是对进程进行调度处理。
首先看一下调度器的定义接口:CoroutineDispatcher
。
添加了两个方法isDispatchNeeded()
和dispatch()
。isDispatchNeeded()
用于分别当前是否需要调度,dispatch()
是调度的执行代码。
CoroutineDispatcher
本身是context
的子类,同时实现了ContinuationInterceptor
接口,在拦截器接口中将回调封装成了DispatchedContinuation
。
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
...
public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true
public abstract fun dispatch(context: CoroutineContext, block: Runnable)
public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
DispatchedContinuation(this, continuation)
}
DispatchedContinuation
是Continuation
的子类,重写了其中的resumeWith()
方法,在其中就调用的CoroutineDispatcher
中新增的两个方法。
如果isDispatchNeeded()
返回True
,标识需要调度,调用dispatcher.dispatch()
实现线程的调度。否则的话,无约束的直接调用回调方法continuation.resumeWith()
。
如果需要控制协程的调度,继承实现CoroutineDispatcher
接口即可。但在实际的使用过程中并不需要我们自己实现,Kotlin
在Dispatchers
内置了四个调度器Main
、Default
、IO
和Unconfined
。
Main
让协程体在主线程运行,Defalut
和IO
在一个线程池中运行,Unconfined
则是不限制运行线程。接下来看他们是如何实现的。
2.1. Main
Main
通过MainDispatcherLoader
获取。
进入到MainDispatcherLoader
,可以发现它的主要作用是创建MainDispatcherFactory
,而MainDispatcherFactory
的作用就是创建MainCoroutineDispatcher
。其次之所以需要factory
创建,是因为Kotlin
有多个平台实现,每个平台实现方法不同。
Android
中的实现是AndroidDispatcherFactory
,创建的调度器实现是handlerContext
,并且看到了熟悉的Looper
,就可以大概猜出是怎么实现的啦。
查看HandlerContext
源码,在isDispatchNeeded()
中根据是否立即调用和是否已经在主线程决定是否需要调度,而在dispatch()
中的调度的实现方法就是调用handler.post()
方法,而handler
就是前面通过Looper.getMainLooper().asHandler()
获取到的MainHandler
,也就是Android
中常说的主线程和UI线程。至于Runnable
被post
之后怎么执行,就是另一个问题,这里不再累赘。
在Android
中,Main
的调度方法就是将所有的任务通过handler.post()
抛到了主线程执行。
2.2. Default
Default
的创建调用了一个叫createDefaultDispatcher()
的方法。createDefaultDispatcher()
直接使用的是DefaultScheduler
。
DefaultScheduler
的实现主要在它的父类ExperimentalCoroutineDispatcher
之中,直接来看它关于调度的两个方法。
首先isDispatchNeeded()
是在基类接口CoroutineDispatcher
之中,返回true
即所有情况下都需要进行调度。
第二个方法dispatch()
实现在ExperimentalCoroutineDispatcher
之中,但是其中所做的事情就是全部代理给了coroutineScheduler
去处理。
进入到coroutineScheduler.dispatch()
,这个方法比较复杂,接下来对这个方法逐行进行分析。
第一个方法createTask()
,是对Runnable
进行了封装,主要是添加了taskContext
,并且在run()
执行完之后调用了taskContext.afterTask()
。在Default
中基本没有使用到这个功能,后续讲到IO
时会用到这玩意。
第二个方法currentWorker()
,调用Thread.currentThread()
获取当前线程,并且强制转换为Worker
返回,这里的Worker
是什么呢?
Worker
是coroutineScheduler
内部对于Thread
的实现,也可以说是对Thread
进行的封装,也即通过Default
调度的协程都是在这些Worker
上面运行的。
再回到这个这个方法,Thread.currentThread() as? Worker
会在什么情况下返回Worker
,又在什么情况下返回了NULL
呢?直接的解释就是当currentThread
是一个Worker
对象的时候会返回Worker
,再深究一下,就是在Default
调度后的协程体中,再次触发了Default
的调度,这个时候返回值才不是空的。举个反例,如果在主线程中触发了Default
的调度,这个方法则会返回NULL
。
第三个方法currentWorker.submitToLocalQueue()
,这是一个扩展方法,字面意思就是往Worker
的localQueue
中塞Task
,这个队列就是线程需要执行的任务队列,但这个方法不一定成功,如果不成功就返回task
,如果成功了就返回了空。
比如刚才获得的currentWorker
是一个空对象,或者线程已经进入终止态,亦或者线程当前阻塞而Task
不阻塞等,都不会塞成功。
接下来如果submitToLocalQueue()
返回值不是空,就会调用addToGlobalQueue()
将Task
添加到coroutineScheduler
自身维护的任务队列之中,这里存放着所有不知道应该交给那个Worker
执行的任务,等待着被拉出来执行。
到了最后一步如果任务是非阻塞的,会调用signalCpuWork()
,在其中调用两个方法尝试获取Worker
。
trtUnpark()
尝试从堆栈中取出一个Worker
,如果成功则调用LockSupport.unpark()
唤醒对应线程。LockSupport
是一个线程阻塞工具类,有一个对应的方法unpark()
和park()
,用来控制线程的唤醒和阻塞,不需要某个对象的锁也不会有先后执行顺序的影响。这里的操作就是将堆栈中阻塞的Worker
唤醒。
如果没有成功从堆栈中拿到Worker
,就会调用tryCreateWorker()
尝试创建一个Worker
。这里只要运行中的线程没有超过设置的上限就会创建新的Worker
,并且如果是首次创建,会同时创建两个,为了实现抢占机制(抢占另一个Worker
后续的任务自己去执行)。这里的corePoolSize
由虚拟机控制。
创建的过程校验了一下上限然后就是创建Worker
并且调用start()
方法启动线程。
前面不论是唤醒或者是start()
,都是走到Worker.run()
方法,在其中通过findTask()
拿到Task
,然后调用executeTask()
执行它。
executeTask()
执行的方法就是调用Runnable.run()
方法,简单。
而findTask()
大多数情况下会走到findAnyTask()
方法,根据标志位和随机数从自己的任务队列或者全局的任务队列中取任务,如果取不到,还会通过trySteal()
遍历其他的Worker
的任务队列,去抢占他们的任务,这些种种都是为了更快的执行任务。
到这里Default
基本上没有什么秘密了,本质还是创建新线程、启动线程或者唤醒线程。
2.3. IO
IO
的调度是基于Default
的,它的实现是LimitingDispatcher
,但是DefaultScheduler
本身却是作为参数传入其中,可以想到,IO
是在Default
的基础上做了一些额外的处理。
进到LimitingDispatcher
,还是直接找dispatch()
方法。
每一次调度的时候,都将计数inFlight
加一,并且和参数parallelism
比较。inFlight
就可以理解为当前正在执行的任务数,而parallelism
就是允许并行的上限,它的值还是由JVM
配置决定的。
如果当前并行的任务数并没有达到上限,就调用传入的Default
调度器的方法,否则就把任务放到自身维护的任务队列中去等待执行。
细心的可能已经发现,这里调用Default
的时候,不是调用的dispatch()
方法,而是dispatchWithContext()
并且把自己传了进去。这里传进去的是TaskContext
接口,前面介绍Task
时候说到,Task
对Runnable
进行了封装,在run()
执行完之后调用了taskContext.afterTask()
。
afterTask()
中,尝试从任务队列中取新任务,如果没有了就将计数器减一。
这也就是IO
新增的逻辑了,增加了并行任务上限的限制,如果没有达到上限,IO
和Default
是一样的效果。
2.4. Unconfined
Unconfined
实现十分简单,isDispatchNeeded()
返回值改成false
。
3. 总结
通过四种调度器的源码分析,这里不再是一个黑魔法。归根溯源之后,其实就是最熟悉的handler.post()
亦或者是Thread.start()
,与我们自己创建一个异步回调没有什么不同。再回归到开头对于协程的解释,它只是将异步所需要的线程调度和传递回调封装或者自动生成,本质上还是异步回调本身,但它将这些繁琐的工作封装起来,用起来还是很舒服的,这次知道了原理之后,可以更舒畅的使用这个工具了。
如果文章中有任何问题,烦请积极指出。
你的点赞和评论是我最大的动力!