从暑假开始学协程相关的东西。刚开始看一脸懵逼,之后重复好像看懂了->诶这个是怎么回事->再看看->好像看懂了->诶这个是咋回事……的循环,现在差不多终于明白了。记下笔记。
一点帮助理解的内容
可以把协程看做一个缩写,全称协作式多例程/多任务(cooperative routine)。看的很多文章把协程和线程对比,是有道理的,对比的是线程的抢占式多任务。也就是说,协程其实是一种多任务调度方式,
这就意味着线程和协程本质上是没法互操作的。毕竟两者的调度方式完全不一样。协程只能和协程之间进行调度。这也是为什么Kotlin中suspend方法只能由suspend方法调用。所以就不要想着从普通的函数中获取suspend函数的结果了。
但是现在并没有直接支持协程的计算体系。所以承载协程的仍然是线程。刚开始的时候非常奇怪协程到底是怎么执行的,所谓的“暂停”到底是什么,为什么不会阻塞执行线程,所谓“一个协程”到底指的是个什么样的实体。我们一个一个来看。
协程如何创建/启动
其实很简单。“协程”这个东西是由某个库函数创建的。在Kotlin中的API是launch
,launch
中会调用(suspend ()->T).startCorountine()
,最终调用到createCorountineUnchecked
。真正的代表协程的对象Continuation
在这个函数里被创建出来。当然这个里面做了一些非常magic的事,其中还需要编译器的帮助。更详细的后面再说。
然后这个Continuation
代表的就是传给launch
的那整个lambda的代码,或者说是一次的完整执行流程,这一整个就是一个“协程”。更明确一点,在这个lambda里调用其他的suspend方法,以及被调用的suspend方法里继续调用的其他suspend方法,都是同一个协程/Continuation。
协程何时会暂停
官方文档这里挺坑的,说是在suspend方法里调用其他的suspend方法会导致当前的协程被暂停。导致我刚开始以为是每个suspend方法就被被包成一个Continuation。其实并不是字面上这样。这个暂停并不是发生在另一个suspend函数被调用的时候,而是发生在那个函数返回的时候。这里要牵扯到协程被转换为状态机的东西,暂时按下不表。总之关键在于一个suspend函数不会像代码写的那样执行和返回。最典型的是suspend suspendCoroutine(block: (Continuation) -> T): T
这个函数。从方法名上它会把当前的执行流暂停,把表示当前执行流的Continuation对象捕获,然后传给block。但其实不一定会导致暂停。就像前面所说的,其实取决于suspend方法是怎么返回的。如果在block里面调用了传入的Continuation的resume
或者resumeWithException
,那么这个方法返回的时候会返回到原来的函数中,继续执行流(或者是抛出异常),返回值(或者异常)是resume/resumeWithException
的参数。示例:
suspend fun foo() {
val a = suspendCoroutine<Int> { cont ->
cont.resume(1)
}
}
但是如果没有调用任何一个即返回,那么就会导致当前执行流被“暂停”。示例
suspend fun foo() {
val a = suspendCoroutine<Int> { cont ->
someAsyncOperation { cont.resume(1) }
}
}
someAsyncOperation
会立即返回,一段时间后再回调cont.resume(1)
这一句。所以,在suspendCoroutine返回时,resume尚未被调用,所以就导致了当前的执行流被“暂停”。
自己写的方法是不可能暂停协程的,一定要调用suspendCorontine
系的几个方法才有可能办得到。原因会在后面说。
暂停是一种什么状态
suspendCorontine
的源码没法告诉我们答案,它调用了suspendCorontineOrReturn
这个没有Kotlin源码的方法(没有源码的一部分原因后面解释)。所以准确的答案不知道,我只能推测个大概。先从执行流的角度说。被暂停的协程会导致最开始的launch直接返回。对,也就是说launch里面调用了f1,f1里面调用了f2,f2再到f3……直到某一个被调用的suspend函数(假如叫fn)里调用了suspendCoroutine
,而且在suspendCoroutine里直接返回了,那fn里suspendCorountine
后面的代码、fn-1中调用fn后面的代码……f1中调用f2之后的代码和launch中调用f1之后的代码,都暂时不会被执行,直接跳过了。launch会直接返回,去执行launch之后的代码。
那么剩下的代码都在哪里呢?都在Continuation里。编译器的魔法施展在这里,会把协程编译成一个状态机然后放在Continuation里,所以Continuation包含了整个协程的代码。它的引用正被持有在那个回调函数里(captured lambda)。等到异步操作完成,调用回调函数,回调函数中调用resume,也就对应着原来的执行流的继续。
那么如果一直没用调用resume呢?Continuation也是普通的对象,所以迟早会被GC掉。这里资源泄露的问题看github上的design document。
在哪里resume
也就是说被暂停的协程解除暂停后会在哪个线程上被继续执行?如果没有特定的interceptor,就会普通地在resume方法被调用的线程上被执行,也就是在异步操作完成后调用回调方法的那个线程上。
当然这个行为可以定制。库中给我们准备好的方式就是用CoroutineDispatcher
,并把它作为Context在launch的时候传进来。它继承自CoroutineInceptor
,库进行了进一步的封装,所以一般只需要重载dispatch
方法即可(needDispatch
官方推荐不重载,保持默认的始终返回true
的行为)。封装十分贴心,把resume和其他一些东西直接包成Runnable
,你只需要把这个Runnable
放到某个线程执行就行了。所以其实和线程池很相似,库里也因此提供了Executor.asDispatcher
的拓展方法。
顺便一提,Vertx提过了一个dispatcher,作用就是让协程始终保持在event loop上执行。所以如果不需要这个效果,可以不使用。