上一篇说完了协程的整个生命周期,总结一下:
- 用Coroutine Builder函数(
launch
,startCoroutine
等)创建协程
- 库内部使用createCoroutineUnchecked来创建协程,需要借助编译器的帮助
- 协程中的指令开始顺序执行
- 调用另一个
suspend
函数,直到调用到suspendCoroutine
系列的函数
- 如果
suspendCoroutine
在同步返回前调用了Continuation.resume/resumeWithException
,则会像普通的函数一样正常的返回/抛出异常,resume
/resumeWithException
的参数会作为返回值/抛出的异常。然后继续执行协程中的代码 - 如果返回前没有调用两者中的任何一个,那么协程中后续的代码会被全部跳过,直接从coroutine builder函数中返回,继续执行调用builder的方法中的后续代码。
suspendCoroutine
系列的方法会调用suspendCoroutineOrReturn
,which is 一个看不到源码内部方法
- 如果
- 如果被suspend了,协程会在未来某个时刻之后其
resume/resumeException
之后被继续执行,这时的行为和在suspendCoroutine
中同步地resume/resumeWithException
的效果一样。唯一的不同之处在于,之后的代码可能在于前半部分不一样的线程上执行。这取决于这个协程的Context中的CoroutineDispatcher的行为。 - 整个协程执行完后,coroutine对象(准确说是Continuation)会普通地被gc掉。如果一个暂停的协程始终没有resume,那么如果它的引用没有被持有,也会被回收掉;否则,将一直存在在内存中。
来一个例子
fun main(args: Array<String>) {
launch {
println(suspendFunc1())
println(suspendFunc2())
println(suspendFunc3())
println("coroutine is about to exit")
}
println("launch has returned")
Thread.sleep(200) // make sure that the main thread won't exit before coroutine exit
}
suspend fun suspendFunc1() : Int {
return suspendCoroutine { cont ->
println("before thread")
thread {
Thread.sleep(100)
cont.resume(1)
}
}
}
suspend fun suspendFunc2() : Int {
return suspendCoroutine { cont ->
cont.resume(2)
}
}
suspend fun suspendFunc3() : Int {
return 3
}
这段会输出
launch has returned
before thread
1
2
3
coroutine is about to exit
过程:
launch
将传入的lambda创建为协程,生成Continuation对象
-> 调用suspendFunc1
,注意此时协程并不会暂停!
-> suspendFunc1
中调用suspendCoroutine
,suspendCoroutine
将代表当前协程的Continuation传给后面的代码段
-> 代码段中新建了一个线程。这个线程会在0.1毫秒后调用Continuation的resume
-> 那么这个代码段就在没有调用resume的情况下直接返回
-> 所以此时这个协程被暂停,从launch
中返回,输出launch has returned
-> 0.1ms后,resume
被调用,1作为suspendCoroutine
的返回值,整个函数继续执行,也就成了suspendFunc1
的返回值。打印1
-> 调用suspendFunc2
,再调用了suspendCoroutine
,这一次代码段返回前调用了resume
,所以协程没有被中断,suspendCoroutine
直接返回了2,然后返回到launch
的里面。打印2
-> 最后调用suspendFunc3
,完全没有调用任何其他函数,所以和普通函数没有任何区别。打印3
-> 最后打印suspend is about to exit
那么before thread
呢?我故意在上面没讲,因为这与CoroutineStart和CoroutineDispatcher的机制有关
CoroutineStart
这表示协程开始时的行为。粗略地说(按照文档的说法),有C#和JS两种风格,前者在协程开始时仍在原来的协程上执行,在第一次暂停时才把协程拿走(就是不在当前线程上继续执行这个协程了),仅仅在resume时才让CoroutineDispatcher调度。后者在协程开始时立即就会用CoroutineDispatcher来将协程调度到某个线程上执行。换句话说,前者的launch
会在第一次暂停时才会返回,后者的则总是会立即返回(准确说是把协程派发走之后)
实际上Kotlin提供了好几种选项,具体行为自己看代码
CoroutineDispatcher
其实上面说完了,CoroutineDispatcher
就是在resume时(也可能在开始时就)调度协程到某个线程上继续执行。具体地说,它本身是一个ContinuationInterceptor,也就是它会拦截某个Continuation的resume操作,返回另一个Continuation(往往是把被拦截的那个包进另一个里面)。所以一般Interceptor也要有一个对应的Continatioin类。这个用来包裹的Continuation用来在调用被包裹的那个的resume之前插入一些逻辑。一个简单的例子:
class WrapperContinuation<T>(private val cont: Continuation<T> : Continuation<T> {
fun resume(value: T) {
someLogic(value)
cont.resume(value)
}
fun resumeWithException(ex: Throwable) {
someLogic(ex)
cont.resumeWithException(ex)
}
}
而CoroutineDispatcher由库进一步封装了,用来包裹的Continuation已经提供了,只需要重载CoroutineDispatcher的dispatch方法就行了。(写到这儿我突然想起来第一篇好像已经提过了这个orz)总之就是把传进来的那个Runnable放到某个线程上执行就行了。
库提供的launch
采用了JS式的CoroutineStart
行为,并且,在CoroutineContext
里如果没有任何的Dispatcher的话,就用一个DefaultDispatcher
作为派发器。这个类会把所有协程都放到CommonPool
里(公用的ForkJoinPool
)。
所以这里就可以来回答上面的问题了。例子中的launch
会立即返回,而before thread
是从一个线程池里的线程打印出来的。所以launch has returned
在before thread
之前打印。
而如果选择使用C#式的CoroutineStart
,那么before thread
就会在launch has returned
之前打印,然后是1 2 3。
(怎么好像和Dispatcher没什么关系orz,算了写了就写了)
好,那整个协程的完整执行过程差不多讲完了。接下来是一些实现细节。