Kotlin 协程源代码泛读:Continuation 思想实验-2

接着上回 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协程基本概念,具体实现的时候,需要考虑很多问题!

相关推荐
_extraordinary_1 小时前
MySQL 事务(二)
android·数据库·mysql
kebijuelun2 小时前
KV cache 缓存与量化:加速大型语言模型推理的关键技术
缓存·语言模型·kotlin
MyhEhud2 小时前
kotlin @JvmStatic注解的作用和使用场景
开发语言·python·kotlin
鸿蒙布道师5 小时前
鸿蒙NEXT开发动画案例5
android·ios·华为·harmonyos·鸿蒙系统·arkui·huawei
橙子1991101611 小时前
在 Kotlin 中什么是委托属性,简要说说其使用场景和原理
android·开发语言·kotlin
androidwork11 小时前
Kotlin Android LeakCanary内存泄漏检测实战
android·开发语言·kotlin
笨鸭先游12 小时前
Android Studio的jks文件
android·ide·android studio
gys989512 小时前
android studio开发aar插件,并用uniapp开发APP使用这个aar
android·uni-app·android studio
H3091912 小时前
vue3+dhtmlx-gantt实现甘特图展示
android·javascript·甘特图
像风一样自由12 小时前
【001】renPy android端启动流程分析
android·gitee