上一篇的kotlin讲到了协程的启动、等待和取消,这一篇对kotlin协程部分内容的补充。
挂起函数
接触到协程之后出现的一个新型的函数,以特殊修饰符suspend修饰的函数被称为挂起函数。挂起函数只能在协程中和其他挂起函数中调用,不能在其他部分使用。并且要启动一个协程,挂起函数是必须的,为了验证可以查看上一篇中提到的launch源码。
/**
* launch实现了Job接口
* @param context 协程上下文,默认值是 [DefaultDispatcher].
* @param start 协程启动方式,默认值是 [CoroutineStart.DEFAULT].
* @param parent 显示指出父Job, 通过[context]重写job.
* @param block 具体协程的代码.
*/
public actual fun launch(
context: CoroutineContext = DefaultDispatcher,
start: CoroutineStart = CoroutineStart.DEFAULT,
parent: Job? = null,
block: suspend CoroutineScope.() -> Unit
): Job {
...
}
可以看到第四个参数block是一个挂起函数,当将一个lambda表达式传给launch时,它就是一个挂起函数。不过这一部分需要引入另一个协程启动函数async,它的源码和launch很类似,不过async是返回了一个Deferred对象,而Deferred又是继承自Job。
public actual fun <T> async(
context: CoroutineContext = DefaultDispatcher,
start: CoroutineStart = CoroutineStart.DEFAULT,
parent: Job? = null,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
...
}
为什么已经有了一套Job-launch-join还需要Deferred-async-await,问题的关键就在Defered虽说继承于Job,但是他略有不同:
public actual interface Deferred<out T> : Job {
...
public actual suspend fun await(): T
}
从上面的代码中明显的看出,Deferred继承Job的基础上增加了out参数T(即有输出),而await函数的作用就是挂起一个协程并返回最后的执行结果,而join函数并没有返回这一项。如果挂起函数是有返回值的,那协程的启动和挂起当然要选择async+await的组合。下面提供一个例子,最后输出的结果是3:
fun main(args: Array<String>) {
runBlocking<Unit> {
val one = async(coroutineContext) { job1() }
val two = async(coroutineContext) { job2() }
println("结果是:${one.await() + two.await()}")
}
}
suspend fun job1(): Int {
return 1
}
suspend fun job2(): Int {
return 2
}
Job与Deferred的对比
已经学习到了两种协程启动的方式,但是他们是有着多种的状态和之间的切换,其实源代码中作者已经把这些都罗列好了,只需要复制粘贴就有了下面的几小结。
Job的几种状态
| 状态 | [isActive] | [isCompleted] | [isCancelled] |
| --------------------------------------- | ---------- | ------------- | ------------- |
| New (optional initial state) | false
| false
| false
|
| Active (default initial state) | true
| false
| false
|
| Completing (optional transient state) | true
| false
| false
|
| Cancelling (optional transient state) | false
| false
| true
|
| Cancelled (final state) | false
| true
| true
|
| Completed (final state) | false
| true
| false
|
Job状态的切换流程
Deferred的几种状态
| 状态 | [isActive] | [isCompleted] | [isCompletedExceptionally] | [isCancelled] |
| --------------------------------------- | ---------- | ------------- | -------------------------- | ------------- |
| New (optional initial state) | false
| false
| false
| false
|
| Active (default initial state) | true
| false
| false
| false
|
| Completing (optional transient state) | true
| false
| false
| false
|
| Cancelling (optional transient state) | false
| false
| false
| true
|
| Cancelled (final state) | false
| true
| true
| true
|
| Resolved (final state) | false
| true
| false
| false
|
| Failed (final state) | false
| true
| true
| false
|
Deferred状态的切换流程
调度和线程
协程上下文包括一个协程调度程序CoroutineDispatcher,他可以指定由哪个线程来执行协程。调度器可以将协程调度到一个线程池,限制在特定的线程中;也可以不做任何限制,让它无约束运行。要去理解这句话,就必须参考launch或async构造函数中的第一个参数(可见第一章)。
runBlocking {
val jobs = arrayListOf<Job>()
jobs += async(Unconfined) { // 1
println("Unconfined is worked in ${Thread.currentThread()}")
}
jobs += async(coroutineContext) { // 2
println("coroutineContext is worked in ${Thread.currentThread()}")
}
jobs += async(CommonPool) { // 3
println("CommonPool is worked in ${Thread.currentThread()}")
}
jobs += async(newSingleThreadContext("newThread")) { // 3
println("newThread is worked in ${Thread.currentThread()}")
}
jobs.forEach { it.join() }
}
- 注释1,使用的Unconfined,一种无限制上下文,协程会在当前调用的栈中执行直到第一次挂起,挂起之后该协程可以被任意一个线程调用以重启;
- 注释2,coroutineContext继承CoroutineContext,在主线程中执行;
- 注释3,CommonPool,上一篇讲过,直接继承于CoroutineDispatcher,在内部创建了一个ExecutorService类型线程池;
- 注释4,新建一个ThreadPoolDispatcher类型的包含一个线程的协程上下文,他可以调度协程到一个固定大小的线程池中。
- 有了上面的解释,程序运行之后的结果也能猜出个几分:
协程的内部机制
协程完全通过编译技术实现(不需要来自VM或OS端的支持),挂起通过代码来生效。基 本上,每个挂起函数(优化可能适用,但我们不在这里讨论)都转换为状态机,其中的状态 对应于挂起调用。刚好在挂起前,下一状态与相关局部变量等一起存储在编译器生成的类的 字段中。在恢复该协程时,恢复局部变量并且状态机从刚好挂起之后的状态进行。
挂起的协程可以作为保持其挂起状态与局部变量的对象来存储和传递。这种对象的类型是 Continuation,而这里描述的整个代码转换对应于经典的延续性传递风格(Continuationpassing style)。因此,挂起函数有一个Continuation类型的额外参数作为高级选项。
总结
协程的一些用法基本上可以先总结这些了,目前协程的一些api仍然是实验中的,不过也是出现了很多新名词。
- runBlocking函数,主要用于桥接阻塞代码(如Thread)和挂起风格非阻塞(如delay)代码,这样可以保证编码风格的统一;
- launch和async函数,都是启动协程的函数,async有返回值,分别返回Job和Deferred对象;
- CoroutineDispatcher函数,作为CoroutineContext的子类,用于对协程的调度。