破解 Kotlin 协程(6) - 协程挂起篇-程序员宅基地

技术标签: ViewUI  java  移动开发  javascript  

关键词:Kotlin 协程 协程挂起 任务挂起 suspend 非阻塞

协程的挂起最初是一个很神秘的东西,因为我们总是用线程的概念去思考,所以我们只能想到阻塞。不阻塞的挂起到底是怎么回事呢?说出来你也许会笑~~(哭?。。抱歉这篇文章我实在是没办法写的更通俗易懂了,大家一定要亲手实践!)

1. 先看看 delay

我们刚刚学线程的时候,最常见的模拟各种延时用的就是 Thread.sleep 了,而在协程里面,对应的就是 delaysleep 让线程进入休眠状态,直到指定时间之后某种信号或者条件到达,线程就尝试恢复执行,而 delay 会让协程挂起,这个过程并不会阻塞 CPU,甚至可以说从硬件使用效率上来讲是“什么都不耽误”,从这个意义上讲 delay 也可以是让协程休眠的一种很好的手段。

delay 的源码其实很简单:


public suspend fun delay(timeMillis: Long) {
    if (timeMillis <= 0) return // don't delay
    return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
        cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
    }
}

复制代码

cont.context.delay.scheduleResumeAfterDelay 这个操作,你可以类比 JavaScript 的 setTimeout,Android 的 handler.postDelay,本质上就是设置了一个延时回调,时间一到就调用 cont 的 resume 系列方法让协程继续执行。

剩下的最关键的就是 suspendCancellableCoroutine 了,这可是我们的老朋友了,前面我们用它实现了回调到协程的各种转换 —— 原来 delay 也是基于它实现的,如果我们再多看一些源码,你就会发现类似的还有 joinawait 等等。

2. 再来说说 suspendCancellableCoroutine

既然大家对于 suspendCancellableCoroutine 已经很熟悉了,那么我们干脆直接召唤一个老朋友给大家:


private suspend fun joinSuspend() = suspendCancellableCoroutine<Unit> { cont ->
    cont.disposeOnCancellation(invokeOnCompletion(handler = ResumeOnCompletion(this, cont).asHandler))
}

复制代码

Job.join() 这个方法会首先检查调用者 Job 的状态是否已经完成,如果是,就直接返回并继续执行后面的代码而不再挂起,否则就会走到这个 joinSuspend 的分支当中。我们看到这里只是注册了一个完成时的回调,那么传说中的 suspendCancellableCoroutine 内部究竟做了什么呢?


public suspend inline fun <T> suspendCancellableCoroutine(
    crossinline block: (CancellableContinuation<T>) -> Unit
): T =
    suspendCoroutineUninterceptedOrReturn { uCont ->
        val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
        block(cancellable)
        cancellable.getResult() // 这里的类型是 Any?
    }
    
复制代码

suspendCoroutineUninterceptedOrReturn 这个方法调用的源码是看不到的,因为它根本没有源码:P 它的逻辑就是帮大家拿到 Continuation 实例,真的就只有这样。不过这样说起来还是很抽象,因为有一处非常的可疑:suspendCoroutineUninterceptedOrReturn 的返回值类型是 T,而传入的 lambda 的返回值类型是 Any?, 也就是我们看到的 cancellable.getResult() 的类型是 Any?,这是为什么?

我记得在协程系列文章的开篇,我就提到过 suspend 函数的签名,当时是以 await 为例的,这个方法大致相当于:

fun await(continuation: Continuation<User>): Any {
    ...
}
复制代码

suspend 一方面为这个方法添加了一个 Continuation 的参数,另一方面,原先的返回值类型 User 成了 Continuation 的泛型实参,而真正的返回值类型竟然是 Any。当然,这里因为定义的逻辑返回值类型 User 是不可空的,因此真实的返回值类型也用了 Any 来示意,如果泛型实参是个可空的类型,那么真实的返回值类型也就是 Any? 了,这正与前面提到的 cancellable.getResult() 返回的这个 Any? 相对应。

如果大家去查 await 的源码,你同样会看到这个 getResult() 的调用。

简单来说就是,对于 suspend 函数,不是一定要挂起的,可以在需要的时候挂起,也就是要等待的协程还没有执行完的时候,等待协程执行完再继续执行;而如果在开始 join 或者 await 或者其他 suspend 函数,如果目标协程已经完成,那么就没必要等了,直接拿着结果走人即可。那么这个神奇的逻辑就在于 cancellable.getResult() 究竟返回什么了,且看:


internal fun getResult(): Any? {
    ...
    if (trySuspend()) return COROUTINE_SUSPENDED // ① 触发挂起逻辑
    ...
    if (state is CompletedExceptionally)  // ② 异常立即抛出
        throw recoverStackTrace(state.cause, this) 
    return getSuccessfulResult(state) // ③ 正常结果立即返回
}

复制代码

这段代码 ① 处就是挂起逻辑了,表示这时候目标协程还没有执行完,需要等待结果,②③是协程已经执行完可以直接拿到异常和正常结果的两种情况。②③好理解,关键是 ①,它要挂起,这返回的是个什么东西?

public val COROUTINE_SUSPENDED: Any get() = CoroutineSingletons.COROUTINE_SUSPENDED

internal enum class CoroutineSingletons { COROUTINE_SUSPENDED, UNDECIDED, RESUMED }
复制代码

这是 1.3 的实现,1.3 以前的实现更有趣,就是一个白板 Any。其实是什么不重要,关键是这个东西是一个单例,任何时候协程见到它就知道自己该挂起了。

3. 深入挂起操作

既然说到挂起,大家可能觉得还是一知半解,还是不知道挂起究竟怎么做到的,怎么办?说真的这个挂起是个什么操作其实一直没有拿出来给大家看,不是我们太小气了,只是太早拿出来会比较吓人。。


suspend fun hello() = suspendCoroutineUninterceptedOrReturn<Int>{
    continuation ->
    log(1)
    thread {
        Thread.sleep(1000)
        log(2)
        continuation.resume(1024)
    }
    log(3)
    COROUTINE_SUSPENDED
}

复制代码

我写了这么一个 suspend 函数,在 suspendCoroutineUninterceptedOrReturn 当中直接返回了这个传说中的白板 COROUTINE_SUSPENDED,正常来说我们应该在一个协程当中调用这个方法对吧,可是我偏不,我写一段 Java 代码去调用这个方法,结果会怎样呢?


public class CallCoroutine {
    public static void main(String... args) {
        Object value = SuspendTestKt.hello(new Continuation<Integer>() {
            @NotNull
            @Override
            public CoroutineContext getContext() {
                return EmptyCoroutineContext.INSTANCE;
            }

            @Override
            public void resumeWith(@NotNull Object o) { // ①
                if(o instanceof Integer){
                    handleResult(o);
                } else {
                    Throwable throwable = (Throwable) o;
                    throwable.printStackTrace();
                }
            }
        });

        if(value == IntrinsicsKt.getCOROUTINE_SUSPENDED()){ // ②
            LogKt.log("Suspended.");
        } else {
            handleResult(value);
        }
    }

    public static void handleResult(Object o){
        LogKt.log("The result is " + o);
    }
}

复制代码

这段代码看上去比较奇怪,可能会让人困惑的有两处:

① 处,我们在 Kotlin 当中看到的 resumeWith 的参数类型是 Result,怎么这儿成了 Object 了?因为 Result 是内联类,编译时会用它唯一的成员替换掉它,因此就替换成了 Object (在Kotlin 里面是 Any?

② 处 IntrinsicsKt.getCOROUTINE_SUSPENDED() 就是 Kotlin 的 COROUTINE_SUSPENDED

剩下的其实并不难理解,运行结果自然就是如下所示了:

07:52:55:288 [main] 1
07:52:55:293 [main] 3
07:52:55:296 [main] Suspended.
07:52:56:298 [Thread-0] 2
07:52:56:306 [Thread-0] The result is 1024
复制代码

其实这段 Java 代码的调用方式与 Kotlin 下面的调用已经很接近了:

suspend fun main() {
    log(hello())
}
复制代码

只不过我们在 Kotlin 当中还是不太容易拿到 hello 在挂起时的真正返回值,其他的返回结果完全相同。

12:44:08:290 [main] 1
12:44:08:292 [main] 3
12:44:09:296 [Thread-0] 2
12:44:09:296 [Thread-0] 1024
复制代码

很有可能你看到这里都会觉得晕头转向,没有关系,我现在已经开始尝试揭示一些协程挂起的背后逻辑了,比起简单的使用,概念的理解和接受需要有个小小的过程。

4. 深入理解协程的状态转移

前面我们已经对协程的原理做了一些揭示,显然 Java 的代码让大家能够更容易理解,那么接下来我们再来看一个更复杂的例子:


suspend fun returnSuspended() = suspendCoroutineUninterceptedOrReturn<String>{
    continuation ->
    thread {
        Thread.sleep(1000)
        continuation.resume("Return suspended.")
    }
    COROUTINE_SUSPENDED
}

suspend fun returnImmediately() = suspendCoroutineUninterceptedOrReturn<String>{
    log(1)
    "Return immediately."
}

复制代码

我们首先定义两个挂起函数,第一个会真正挂起,第二个则会直接返回结果,这类似于我们前面讨论 join 或者 await 的两条路径。我们再用 Kotlin 给出一个调用它们的例子:


suspend fun main() {
    log(1)
    log(returnSuspended())
    log(2)
    delay(1000)
    log(3)
    log(returnImmediately())
    log(4)
}

复制代码

运行结果如下:

08:09:37:090 [main] 1
08:09:38:096 [Thread-0] Return suspended.
08:09:38:096 [Thread-0] 2
08:09:39:141 [kotlinx.coroutines.DefaultExecutor] 3
08:09:39:141 [kotlinx.coroutines.DefaultExecutor] Return immediately.
08:09:39:141 [kotlinx.coroutines.DefaultExecutor] 4
复制代码

好,现在我们要揭示这段协程代码的真实面貌,为了做到这一点,我们用 Java 来仿写一下这段逻辑:

注意,下面的代码逻辑上并不能做到十分严谨,不应该出现在生产当中,仅供学习理解协程使用。


public class ContinuationImpl implements Continuation<Object> {

    private int label = 0;
    private final Continuation<Unit> completion;

    public ContinuationImpl(Continuation<Unit> completion) {
        this.completion = completion;
    }

    @Override
    public CoroutineContext getContext() {
        return EmptyCoroutineContext.INSTANCE;
    }

    @Override
    public void resumeWith(@NotNull Object o) {
        try {
            Object result = o;
            switch (label) {
                case 0: {
                    LogKt.log(1);
                    result = SuspendFunctionsKt.returnSuspended( this);
                    label++;
                    if (isSuspended(result)) return;
                }
                case 1: {
                    LogKt.log(result);
                    LogKt.log(2);
                    result = DelayKt.delay(1000, this);
                    label++;
                    if (isSuspended(result)) return;
                }
                case 2: {
                    LogKt.log(3);
                    result = SuspendFunctionsKt.returnImmediately( this);
                    label++;
                    if (isSuspended(result)) return;
                }
                case 3:{
                    LogKt.log(result);
                    LogKt.log(4);
                }
            }
            completion.resumeWith(Unit.INSTANCE);
        } catch (Exception e) {
            completion.resumeWith(e);
        }
    }

    private boolean isSuspended(Object result) {
        return result == IntrinsicsKt.getCOROUTINE_SUSPENDED();
    }
}

复制代码

我们定义了一个 Java 类 ContinuationImpl,它就是一个 Continuation 的实现。

实际上如果你愿意,你还真得可以在 Kotlin 的标准库当中找到一个名叫 ContinuationImpl 的类,只不过,它的 resumeWith 最终调用到了 invokeSuspend,而这个 invokeSuspend 实际上就是我们的协程体,通常也就是一个 Lambda 表达式 —— 我们通过 launch启动协程,传入的那个 Lambda 表达式,实际上会被编译成一个 SuspendLambda 的子类,而它又是 ContinuationImpl 的子类。

有了这个类我们还需要准备一个 completion 用来接收结果,这个类仿照标准库的 RunSuspend 类实现,如果你有阅读前面的文章,那么你应该知道 suspend main 的实现就是基于这个类:


public class RunSuspend implements Continuation<Unit> {

    private Object result;

    @Override
    public CoroutineContext getContext() {
        return EmptyCoroutineContext.INSTANCE;
    }

    @Override
    public void resumeWith(@NotNull Object result) {
        synchronized (this){
            this.result = result;
            notifyAll(); // 协程已经结束,通知下面的 wait() 方法停止阻塞
        }
    }

    public void await() throws Throwable {
        synchronized (this){
            while (true){
                Object result = this.result;
                if(result == null) wait(); // 调用了 Object.wait(),阻塞当前线程,在 notify 或者 notifyAll 调用时返回
                else if(result instanceof Throwable){
                    throw (Throwable) result;
                } else return;
            }
        }
    }
}

复制代码

这段代码的关键点在于 await() 方法,它在其中起了一个死循环,不过大家不要害怕,这个死循环是个纸老虎,如果 resultnull,那么当前线程会被立即阻塞,直到结果出现。具体的使用方法如下:


...
    public static void main(String... args) throws Throwable {
        RunSuspend runSuspend = new RunSuspend();
        ContinuationImpl table = new ContinuationImpl(runSuspend);
        table.resumeWith(Unit.INSTANCE);
        runSuspend.await();
    }
...

复制代码

这写法简直就是 suspend main 的真实面貌了。

我们看到,作为 completion 传入的 RunSuspend 实例的 resumeWith 实际上是在 ContinuationImplresumeWtih 的最后才会被调用,因此它的 await() 一旦进入阻塞态,直到 ContinuationImpl 的整体状态流转完毕才会停止阻塞,此时进程也就运行完毕正常退出了。

于是这段代码的运行结果如下:

08:36:51:305 [main] 1
08:36:52:315 [Thread-0] Return suspended.
08:36:52:315 [Thread-0] 2
08:36:53:362 [kotlinx.coroutines.DefaultExecutor] 3
08:36:53:362 [kotlinx.coroutines.DefaultExecutor] Return immediately.
08:36:53:362 [kotlinx.coroutines.DefaultExecutor] 4
复制代码

我们看到,这段普通的 Java 代码与前面的 Kotlin 协程调用完全一样。那么我这段 Java 代码的编写根据是什么呢?就是 Kotlin 协程编译之后产生的字节码。当然,字节码是比较抽象的,我这样写出来就是为了让大家更容易的理解协程是如何执行的,看到这里,相信大家对于协程的本质有了进一步的认识:

  • 协程的挂起函数本质上就是一个回调,回调类型就是 Continuation
  • 协程体的执行就是一个状态机,每一次遇到挂起函数,都是一次状态转移,就像我们前面例子中的 label 不断的自增来实现状态流转一样

如果能够把这两点认识清楚,那么相信你在学习协程其他概念的时候就都将不再是问题了。如果想要进行线程调度,就按照我们讲到的调度器的做法,在 resumeWith 处执行线程切换就好了,其实非常容易理解的。官方的协程框架本质上就是在做这么几件事儿,如果你去看源码,可能一时云里雾里,主要是因为框架除了实现核心逻辑外还需要考虑跨平台实现,还需要优化性能,但不管怎么样,这源码横竖看起来就是五个字:状态机回调。

5. 小结

不同以往,我们从这一篇开始毫无保留的为大家尝试揭示协程背后的逻辑,也许一时间可能有些难懂,不过不要紧,你可以使用协程一段时间之后再来阅读这些内容,相信一定会豁然开朗的。

当然,这一篇内容的安排更多是为后面的序列篇开路,Kotlin 的 Sequence 就是基于协程实现的,它的用法很简单,几乎与普通的 Iterable 没什么区别,因此序列篇我们会重点关注它的内部实现原理,欢迎大家关注。


欢迎关注 Kotlin 中文社区!

中文官网:www.kotlincn.net/

中文官方博客:www.kotliner.cn/

公众号:Kotlin

知乎专栏:Kotlin

CSDN:Kotlin中文社区

掘金:Kotlin中文社区

简书:Kotlin中文社区

转载于:https://juejin.im/post/5ceb494851882532b93019e2

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_33856370/article/details/91429056

智能推荐

王者荣耀最低战力查询接口_王者荣耀战力查询网址-程序员宅基地

文章浏览阅读1.1w次,点赞5次,收藏15次。分享一个可以查王者荣耀苹果区战力的接口,数据每周一会更新接口地址 支持GET、POSThttps://gamehook.top/api/hero/select所需参数:auth_code 授权码(身份验证) f0845d444f04eb24 必传hero_name 英雄名称 支持模糊搜索 必传返回结构{“code”:0,“msg”:“查询成功”,“data”:{“id”:19,“hero_name”:“曜”,“hero_photo”:“https://gamehook.top//u_王者荣耀战力查询网址

计算机考博面试题,考博复试八大常见问题-程序员宅基地

文章浏览阅读583次。“考博复试八大常见问题”相信是准备参加医学考博的朋友比较关心的事情,为此,医学教育网小编整理内容如下:第 1 问:初试和复试哪个更重要?初试成绩是敲门砖,对最终是否被录取有一定的参考价值,但即使在初试中考出高分也是属于过去,导师还要结合复试成绩来作出最终决定(这个是显而易见的,不然还要复试干嘛)。换言之,初试成绩决定不了录取,对录取的影响有限,但是话又说回来,前提是你要有复试机会!第 2 问:自我..._计算机申博面试题

Maven异常处理 Caused by: org.apache.maven.plugin.MojoFailureException: There are test failures.-程序员宅基地

文章浏览阅读8.5k次。Caused by: org.apache.maven.plugin.MojoFailureException: There are test failures.在mvn install打包安装到本地仓库时 出现了这个错误 仔细查找 是因为单元测试没有通过 存在失败的断言 将断言调试成功 即可安装_caused by: org.apache.maven.plugin.mojofailureexception: there are test fail

TCP的三次握手(建立连接)和四次挥手(关闭连接)http://www.cnblogs.com/Jessy/p/3535612.html_net.ipv4.tcp_syncookies与性能 site:blog.csdn.net-程序员宅基地

文章浏览阅读626次。转自:http://www.cnblogs.com/Jessy/p/3535612.htmlTCP的三次握手(建立连接)和四次挥手(关闭连接)参照:http://course.ccniit.com/CSTD/Linux/reference/files/018.PDFhttp://hi.baidu.com/raycomer/item/944d23d9b502d13be3108_net.ipv4.tcp_syncookies与性能 site:blog.csdn.net

五月IDO第三弹,13个热门项目即将上线-程序员宅基地

文章浏览阅读645次。波卡生态正在逐渐发力。作者 | 秦晓峰 编辑 | 郝方舟出品 | Odaily星球日报(ID:o-daily)过去这周的市场,堪称魔幻。随着 SHIB 价格的冲高,PIG、Lion 等...

BATT入局,小程序成超级APP连接一切的枢纽-程序员宅基地

文章浏览阅读358次。小程序已经无所不在的侵入了我们的生活。刚刚诞生时,大家认为微信小程序不过是升级版的“服务号”,也有人把它看作“借尸还魂”的直达号或H5网页。随着微信小程序在我们生活中的作用越来越大,业界此前的诸多偏见..._batt防三方

随便推点

kettle-如何将作业(job)中设置的参数值,传递到子转换(ktr)脚本_kettle9.0 参数无法传递给子转换-程序员宅基地

文章浏览阅读1w次,点赞5次,收藏16次。用途如何将作业(job)中设置的参数值,传递到子转换(ktr)脚本1、作业总体流程1.1、作业命名参数1.2、设置变量-步骤/* 如何将作业(job)中设置的参数值,传递到子转换(ktr)脚本功能:获取或设置变量作用域:仅当前作业(job)有效parent_job.setVariableparent_job.getVariable功能:获取或设置参数变量作用域:作业(..._kettle9.0 参数无法传递给子转换

Linxu界面之如何使侧边栏自动隐藏?(Ubuntu)_如何隐藏ubuntu侧边栏-程序员宅基地

文章浏览阅读5.5k次。如果你装了ubuntu桌面版就会发现默认情况下桌面左边会有一个侧边栏,从中我们可以启动一些软件,但是这个侧边栏一直显示在桌面左边不是很美观,还会占据桌面的的空间。我们可以使其自动隐藏起来,当我们需要时只要把鼠标放到桌面左边它就会出现。1. 系统设置-外观打开系统设置,打开外观选项2. 点击&amp;amp;amp;amp;quot;行为&amp;amp;amp;amp;quot;标签3. 打开自动&amp;amp;amp;amp;quot;隐藏启动器&a_如何隐藏ubuntu侧边栏

JSP九大内置对象详解全析(三):session对象_session撖寡情-程序员宅基地

文章浏览阅读1.1w次,点赞11次,收藏68次。1、session对象概述 session对象是由服务器自动创建与用户请求相关的对象。服务器会为每一个用户创建一个session对象用来保存用户信息,跟踪用户操作。该对象内部使用Map类来保存数据,因此它的数据类型是key-value形式。对应javax.servlet.http.HttpSession.class对象。 服务器为不同的浏览器在内存中创建用于保存数据的对象叫seesio_session撖寡情

USB加密狗复制克隆软件_u盘加密狗复制克隆软件-程序员宅基地

文章浏览阅读3.2w次,点赞6次,收藏50次。前言: 加密狗复制技术现在已经很成熟了,如果您想把手上某一个USB加密狗复制一下,不妨先自己尝试一下复制,实在复制不了,可以请专业人士帮忙(比较知名的如COPYONE工作室)。今天软件小编已经帮大家找来了,就是这款USB加密狗克隆复制软件。下载地址:https://download.csdn.net/download/QQ528621124/19774921加密狗复制克隆其实说白了就是加密狗复制或者模拟,加密狗就像一个U盘一样,存储了特殊的加密串。加密狗是外形酷似U盘的一种硬..._u盘加密狗复制克隆软件

C语言数据结构-第六章、树和二叉树-电大同步进度_给出树的广义表形式怎样看度是多少-程序员宅基地

文章浏览阅读953次,点赞3次,收藏3次。第六章、树和二叉树——内容简介线性结构中结点间具有惟一前驱、惟一后继关系,而非线性结构中结点间前驱、后继的关系并不具有惟一性。其中,在树型结构中结点间关系是前驱惟一而后继不惟一,即结点之间是一对多的关系;直观地看,树结构是指具有分支关系的结构(其分叉、分层的特征类似于自然界中的树)。树和图的理论基础属离散数学内容,数据结构讨论的重点在树和图结构的实现技术。本章主要讨论树结构的特性、存储及其操作的实现。第1讲树的基本概念——内容简介本节主要介绍:树的基本概念树相关的术语_给出树的广义表形式怎样看度是多少

计算机单片机考试作弊检讨书,作弊检讨书500字范文(8页)-原创力文档-程序员宅基地

文章浏览阅读176次。作弊检讨书500字范文作弊实在不是一件明智之举的事,犯了作弊这个错误,该怎么写检讨呢?下面为大家精心了作弊检讨书500字范文,仅供参考。亲爱的班主任:我是高二14班的一名普通学生,写这封检讨书反省我在这次期中考试中作弊的错误。我怀着十万分的愧疚和十万分的难过给你写下这封检讨书:关于此次期中考试,我完全是因为平时上课的不认真,老师布置的作业没有按时完成,因为对于这次考试没有十足的把握,又怕考不好被父..._单片机考试发草稿纸

推荐文章

热门文章

相关标签