学习kotlin有一段时间了,写个博客记录一下。本文基于kotlin 1.3.31来写,如有任何描述不当的地方,请多多指教纠正。
协程是一种轻量级的线程。那和线程有什么不一样?一.线程的调度是由操作系统负责的,它的睡眠、等待、唤醒都是由操作系统控制,开发者无法决定,而协程则可以由开发者决定代码切换的时机;二.线程要放弃CPU使用时间必须要进入阻塞状态,但阻塞的代价是昂贵的,而相比较而言协程可以通过挂起来切换代码,它相比阻塞而言,代价小很多;三.协程运行在线程中,一个线程中可以包含多个协程。
fun main(){
//启动一个协程并且不会阻塞当前线程
GlobalScope.launch {
println("start a Coroutine in GlobalScope.launch ")
}
//启动一个协程并且阻塞当前线程,直到协程里面所有任务完成
//它被设计作为阻塞代码块和挂起风格的库之间的一个桥梁,因此一般用在main
//函数或者测试代码中,而不应该在协程中启动。
runBlocking {
println("start a Coroutine in runBlocking")
}
}
在协程内可使用delay方法或者其他带suspend修饰符的方法表示挂起。带suspend修饰符的方法只能用在协程内,否则会编译报错。
可以用一个变量引用该协程返回的值(Job),然后在启动该协程的父协程内的合适的位置调用Job的join方法,那么该作业就会在合适的时机调用。
fun main(){
GlobalScope.launch {
println("start a Coroutine in GlobalScope.launch ")
//开启一个运行在线程池中的协程
val b= async{
println(" async start")
println("async end")
}
println("GlobalScope.launch running")
b.join()
println("end a Coroutine in GlobalScope.launch ")
}
}
//输出结果:
// start a Coroutine in GlobalScope.launch
// GlobalScope.launch running
// async start
// async end
// end a Coroutine in GlobalScope.launch
协程内部是怎么实现的?使用delay之类的挂起函数,协程是怎么挂起以及恢复的?
接下来从一段代码开始分析:
// 新建并启动 blocking 协程,运行在 main 线程上,等待所有子协程运行完成后才会结束
fun main() = runBlocking<Unit> {
println("${Thread.currentThread().name} : runBlocking start")
// 新建并启动 async 协程,运行在 Dispatchers.Default 的线程池中
val b = async(Dispatchers.Default) {
println("${Thread.currentThread().name} : async start")
println("${Thread.currentThread().name} : async end")
}
b.await()
println("${Thread.currentThread().name} : runBlocking end")
}
从runBlocking开始,主要的流程如下:
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
val currentThread = Thread.currentThread()
val contextInterceptor = context[ContinuationInterceptor]
val eventLoop: EventLoop?
val newContext: CoroutineContext
if (contextInterceptor == null) {
// create or use private event loop if no dispatcher is specified
eventLoop = ThreadLocalEventLoop.eventLoop
newContext = GlobalScope.newCoroutineContext(context + eventLoop)
} else {
// See if context's interceptor is an event loop that we shall use (to support TestContext)
// or take an existing thread-local event loop if present to avoid blocking it (but don't create one)
eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
?: ThreadLocalEventLoop.currentOrNull()
newContext = GlobalScope.newCoroutineContext(context)
}
//创建协程
val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
//启动协程
coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
//循环获取要处理的事件
return coroutine.joinBlocking()
}
协程的EventLoop用ThreadLocal保存,也就是归每个线程所独有,并且在这里默认值为BlockingEventLoop。EventLoop的作用是管理当前协程的事件,它的一些关键方法都在它的子类EventLoopImplBase里,并且EventLoop也是CoroutineContext中的一个元素;而CoroutineContext用来存放一些当前协程运行时需要用到的东西。
runBlocking生成的协程是BlockingCoroutine,它继承于AbstractCoroutine,BlockingCoroutine只有简单的两个方法,有一个joinBlocking在协程启动后就调用,后续再看看这方法。看来协程主要的一些逻辑都在AbstractCoroutine中了。
private class BlockingCoroutine<T>(
parentContext: CoroutineContext,
private val blockedThread: Thread,
private val eventLoop: EventLoop?
) : AbstractCoroutine<T>(parentContext, true) {
override val cancelsParent: Boolean
get() = false // it throws exception to parent instead of cancelling it
override fun afterCompletionInternal(state: Any?, mode: Int) {
// wake up blocked thread
if (Thread.currentThread() != blockedThread)
LockSupport.unpark(blockedThread)
}
@Suppress("UNCHECKED_CAST")
fun joinBlocking(): T {
...
}
}
协程调用start方法后,通过debug调试跟踪,最后来到了Cancellable.kt文件的startCoroutineCancellable方法里。
internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(receiver: R, completion: Continuation<T>) =
runSafely(completion) {
createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellable(Unit)
}
接下来分析一下这几个方法的作用:
createCoroutineUnintercepted():创建一个非拦截的Continuation。里面最终会调用create()来创建,create()代码是在kotlin编译之后生成的,它会返回一个继承BaseContinuationImpl的Continuation,这个BaseContinuationImpl包含了runBlocking闭包里面要执行的代码。这部分后续再详细分析。
intercepted():方法最终跑到ContinuationImpl里面。使用Context[ContinuationInterceptor].interceptContinuation对第一步返回的Continuation进行拦截,也就是返回一个包装后的Continuation。Coroutine[ContextContinuationInterceptor]就是CoroutineDispatcher,具体实现就是上面创建的BlockingEventLooper。它的interceptContinuation方法直接返回一个DispatchedContinuation对象,DispatchedContinuation既是一个Continuation也是一个Runnable。
resumeCancellable():调用上一步返回的DispatchedContinuation的方法,里面使用一开始创建的BlockingEventLooper.dispatch把DispatchedContinuation作为Runnable传进去。dispatch是BlockingEventLooper父类EventLoopImplBase的一个final方法,在方法里面把DispatchedContinuation放到全局参数_queue队列里。
从上面分析可得知,kotlin编译器把我们写在闭包里面的代码以一个BaseContinuationImpl返回给我们,然后再用一个DispatchedContinuation对BaseContinuationImpl进行了代理,还有就是EventLoopImplBase是负责任务队列的管理。
放进去的任务什么时候取出来执行呢?我们回到runBlocking代码块里。执行完Coroutine.start方法后,紧接着执行joinBlocking。joinBlocking是BlockingCoroutine里面的一个方法,我们来看看它的具体实现。
fun joinBlocking(): T {
....
try {
....
try {
//循环取出任务来执行
while (true) {
@Suppress("DEPRECATION")
if (Thread.interrupted()) throw InterruptedException().also { cancelCoroutine(it) }
val parkNanos = eventLoop?.processNextEvent() ?: Long.MAX_VALUE
// note: process next even may loose unpark flag, so check if completed before parking
if (isCompleted) break
...
}
} finally { // paranoia
...
}
} finally { // paranoia
...
}
// now return result
...
return state as T
}
关键就在eventLoop?.processNextEvent() ,我们看看EventLoopImplBase对这方法的具体实现:
override fun processNextEvent(): Long {
// unconfined events take priority
if (processUnconfinedEvent()) return nextTime
// queue all delayed tasks that are due to be executed
val delayed = _delayed.value
if (delayed != null && !delayed.isEmpty) {
....
}
// then process one event from queue
dequeue()?.run()
return nextTime
}
private fun dequeue(): Runnable? {
_queue.loop { queue ->
when (queue) {
null -> return null
is Queue<*> -> {
val result = (queue as Queue<Runnable>).removeFirstOrNull()
if (result !== Queue.REMOVE_FROZEN) return result as Runnable?
_queue.compareAndSet(queue, queue.next())
}
else -> when {
queue === CLOSED_EMPTY -> return null
else -> if (_queue.compareAndSet(queue, null)) return queue as Runnable
}
}
}
}
从这里可看出,之前放进queue的Runnable,现在又取出来并且调用run()来执行了。DispatchedContinuation并没有实现run方法,具体实现在父类DispatchedTask里。
public final override fun run() {
val taskContext = this.taskContext
var exception: Throwable? = null
try {
val delegate = delegate as DispatchedContinuation<T>
val continuation = delegate.continuation
val context = continuation.context
val job = if (resumeMode.isCancellableMode) context[Job] else null
val state = takeState() // NOTE: Must take state in any case, even if cancelled
withCoroutineContext(context, delegate.countOrElement) {
if (job != null && !job.isActive) {
val cause = job.getCancellationException()
cancelResult(state, cause)
continuation.resumeWithStackTrace(cause)
} else {
val exception = getExceptionalResult(state)
if (exception != null)
continuation.resumeWithStackTrace(exception)
else
continuation.resume(getSuccessfulResult(state))
}
}
} catch (e: Throwable) {
// This instead of runCatching to have nicer stacktrace and debug experience
exception = e
} finally {
val result = runCatching { taskContext.afterTask() }
handleFatalException(exception, result.exceptionOrNull())
}
}
这里的continuation就是之前的BaseContinuationImpl,执行resume方法后,就会调用Continuation的resumeWith。BaseContinuationImpl实现了resumeWith,并且是一个final的方法,具体如下:
public final override fun 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.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的resumeWith中,关键方法在invokeSuspend,它是BaseContinuationImpl的抽象方法,具体由kotlin编译器实现。invokeSuspend里面执行的就是runBlocking的闭包代码。invokeSuspend还有一个返回值,当返回COROUTINE_SUSPENDED的时候就会跳出循环,那什么时候会返回COROUTINE_SUSPENDED呢?当闭包里面有挂起函数时(如delay),就会返回该值跳出循环。invokeSuspend的实现是一个状态机,就类似于switch case,通过一个状态值跳转来执行闭包里面每一行代码。状态机的实现后续再作分析,接下来继续往下看,假如completion是一个BaseContinuationImpl,则继续递归执行以上代码,否则的话运行completion.resumeWith。这里的completion实际上就是一开始创建的BlockingCoroutine,前面说了BlockingCoroutine只有简单的两个方法,resumeWith是在它父类AbstractCoroutine里面实现。
public final override fun resumeWith(result: Result<T>) {
makeCompletingOnce(result.toState(), defaultResumeMode)
}
这里的resumeWith也是一个final方法,后续的一些调用都进入它的父类JobSupport里面。JobSupport通过一个_state来管理当前协程的状态以及分发恢复挂起的协程。
说到这里,我们对协程的启动运行过程有了个大概的印象,总结一下启动过程:
在前面执行runBlocking的闭包的时候,通过async也创建了一个协程。它的流程和runBlocking差不多,这里只说明一下它的几个不同点:
调用了await之后,协程是怎么挂起以及恢复的呢?跟踪一下await的代码,最终来到了JobSupport的awaitSuspend。
private suspend fun awaitSuspend(): Any? = suspendCoroutineUninterceptedOrReturn { uCont ->
/*
* Custom code here, so that parent coroutine that is using await
* on its child deferred (async) coroutine would throw the exception that this child had
* thrown and not a JobCancellationException.
*/
val cont = AwaitContinuation(uCont.intercepted(), this)
cont.disposeOnCancellation(invokeOnCompletion(ResumeAwaitOnCompletion(this, cont).asHandler))
cont.getResult()
}
在awaitSuspend中调用了suspendCoroutineUninterceptedOrReturn,这个方法找不到具体实现,猜测也是编译时生成的代码。里面的参数就是一个实现了BaseContinuationImpl的Continuation,也就是说里面会执行await方法后面的代码。suspendCoroutineUninterceptedOrReturn里面的几个方法就不再展开分析,大概的流程就是用一个AwaitContinuation封装了BaseContinuationImpl,然后再扔到DeferredCoroutine的_state队列里去。
说了这么多,再来梳理一下整个流程:
在这过程中AbstractCoroutine的作用是管理协程状态以及分发被挂起的协程,BaseContinuationImpl的作用是用状态机切换执行闭包的代码。
参考资料:
https://www.kotlincn.net/docs/reference/coroutines/basics.html
http://johnnyshieh.me/posts/kotlin-coroutine-deep-diving/