接着上回 Continuation 思想实验,我们来思考另一个问题,如何实现 download
kotlin
fun download(url: String, completion: Continuation) {
// 下载中...
completion.resumeWith(...)
}
聪明的你一定能想到各种各样的方法,最简单粗暴的做法,可以这样
kotlin
fun download(url: String, completion: Continuation) {
val thread = new Thread {
// 下载中...
completion.resumeWith(...)
}
}
这种史前时代的写法,Code Review 的时候会被提 N 个问题,简单改进一下
kotlin
val dispatchers = Executors.newCachedThreadPool()
fun download(url: String, completion: Continuation) {
dispatchers.execute {
// 下载中...
completion.resumeWith(...)
}
}
稍微好点,但是还是有一堆问题,我们点到为止
其它类似的耗时函数,比如 decode 也需要进行类似的改造。代码书写信条:DO NOT REPEAT YOURSELF!告诉你这些函数有相同的样板代码(Executor.execute),而且它和具体的业务(下载,解码)无关,所以更应出现在框架层。那么有没有其它解法呢?
Continuation 思想实验提到带 Continue 参数的方法就像状态机,Continuation.resumeWith 可以在 方法内不同的 label(挂起点)「恢复」执行,我们来看看 download 的调用者 display:
kotlin
fun display(url: String, completion: Continuation? = null) {
var $continuation: Continuation? = null
if (continuation is ContinuationImpl) {
// 自己人!(递归调用)
} else {
// 异乡人!,注意这里保存了 completion
$continuation = ContinuationImpl(completion)
}
// 定义一个内部类
inner class ContinuationImpl(
completion: Continuation
): Continuation {
var label: Int = 0
var result: Any? = null
// resumeWith 让 display 从某个 label 恢复执行
fun resumeWith(result: Any?) {
completion.ressult = result
display(url, completion)
}
}
when (continuation.label) {
0 -> {
// 下次 resume 该执行 1 了
continuation.label = 1
download(url, continuation)
return
}
1 -> {
// 下次 reusme 执行 2,when 代码块之后剩下的代码块
continuation.label = 2
decode(continuation.result as ByteArray, continuation)
return
}
}
show(...)
continuation?.resumeWith(...)
}
这个 resumeWith 就是突破口! resume 有恢复、唤醒的意思,想象你坐卧铺车从一个乡下到城市,上车后你睡下(suspend)了,然后睡一觉在城里的火车站醒来(resume)。我们如何实现这种在一个线程挂起,然后其它线程恢复的效果呢?答案是:
bash
调用者(caller)进行线程切换
使用代理模式,偷梁换柱,拦截(intercepted)resumeWith 调用
kotlin
class DispatchedContinuation(
val executor: Executor,
val delegate: Continuation
): Continuation, Runnable {
fun resumeWith(result: Any) {
executor.submit {
delegate.resumeWith(result)
}
}
}
为了方便使用,定义一个扩展方法
kotlin
fun Continuation.intercepted(executor: Executor): Continuation {
return DispatchedContinuation(this)
}
修改 display 方法:
kotlin
fun display(url: String, executor: Executor, completion: Continuation? = null) {
var $continuation: Continuation? = null
if (continuation is ContinuationImpl) {
// 自己人!(递归调用)
} else {
// 异乡人!,注意这里保存了 completion
// 对 ContinuationImpl 进行 wrap,这样就可以在 Executor 中 resume 啦
$continuation = ContinuationImpl(completion).intercepted(executor)
}
...
}
这个 executor 参数,所有的异步方法都需要,进一步抽象&封装:
kotlin
interface Scope {
val context: Context
}
interface Context {
val executor: Executor
}
fun Scope.display(url: String, completion: Continuation? = null) {
...
}
这样可以让调用者指定 executor,是不是有那么一丢丢协程的味道了! 在我们上述思想的过程中,已经触及到协程中一个重要的概念 Dispatcher(调度器),以及它是如何和 Continuation 协作的
最后再考虑一个问题,目前的封装可以让 display 在指定的 Executor 中 resume,但是 diaplay 第一个 resume 之前(when 语句块中的 label 0)呢?我们可以指定 display 在哪个线程启动吗? 我们把 display 也变成状态机:
kotlin
class DisplayContinuation(
val scope: Scope,
): Continuation {
fun resumeWith(result: Any?) {
invokeSuspend(result as url)
}
fun invokeSuspend() {
// 这里有点问题
scope.display(url, this)
}
}
ok,现在对 display 的调用,变成这样
scss
DisplayContinuation(scope).resumeWith(...)
如果想在工作线程中调用(调度)display,就可以这样写
scss
DisplayContinuation(scope).intercepted().resumeWith(...)
如果又来了个其它的任务,比如下载和更新应用,你也可以写一个类似的 UpdateContinuation,它们两有重复(相似)的代码,所以可以提取一个基类
kotlin
class BaseContinuationImpl: Continuation {
fun resumeWith(result: Any?) {
invokeSuspend(result)
}
abstract fun invokeSuspend(param: Any?)
}
DisplayContinuation: BaseContinuationImpl {...}
如果这么抽象和封装下去,你会搞出个类似 launch 的协程启动器...
这些近乎玩具的代码,只是为了展示Kotlin协程基本概念,具体实现的时候,需要考虑很多问题!