• Kotlin 朱涛15 协程 挂起函数 suspend CPS Continuation


    本文地址


    目录

    15 | 挂起函数:Kotlin协程的核心

    初步了解挂起函数

    Kotlin 协程最大的优势,就在于它的挂起函数

    • 虽然很多编程语言都有协程的特性,但到目前为止,只有 Kotlin 引入了挂起函数的概念
    • 尽管有些语言的协程底层,也存在挂起恢复的概念,但是 Kotlin 是唯一将这一概念直接暴露给开发者,直接用于修饰一个函数的

    Java 的回调地狱

    getUserInfo(new CallBack() {                 // 发起一个异步任务
        @Override
        public void onSuccess(String response) { // 通过 CallBack 返回 response
            if (response != null) {
                System.out.println(response);
            }
        }
    });
    

    连续发起多个异步任务:

    getUserInfo(new CallBack() {
        @Override
        public void onSuccess(String user) {
            if (user != null) {
                System.out.println(user);
                getFriendList(user, new CallBack() {
                    @Override
                    public void onSuccess(String friendList) {
                        if (friendList != null) {
                            System.out.println(friendList);
                            getFeedList(friendList, new CallBack() {
                                @Override
                                public void onSuccess(String feed) {
                                    if (feed != null) {
                                        System.out.println(feed);
                                    }
                                }
                            });
                        }
                    }
                });
            }
        }
    });
    

    以上代码存在诸多缺陷:可读性差、扩展性差、维护性差,极易出错!

    如果让你基于以上代码再扩展出 超时取消 出错重试 进度展示 等相关功能,你会不会觉得头疼?

    使用挂起函数重构

    使用 Kotlin 协程的挂起函数,重构上面的代码:

    val user = getUserInfo()
    val friendList = getFriendList(user)
    val feedList = getFeedList(friendList)
    

    这就是 Kotlin 协程的魅力:以同步的方式完成异步任务

    以上代码之所以能写成类似同步的方式,关键在于这三个函数的定义:

    suspend fun getUserInfo(): String { // 定义为【挂起函数】
        withContext(Dispatchers.IO) {   // 控制协程执行的线程池,后面再讲
            delay(1000L)                // 模拟网络请求耗时
        }
        return "BoyCoder"               // 模拟网络请求结果
    }
    
    suspend fun getFriendList(user: String): String {...} // 挂起函数
    suspend fun getFeedList(list: String): String {...}   // 挂起函数
    

    挂起函数执行流程

    所谓的挂起函数,其实就是比普通的函数多了一个suspend关键字而已。挂起函数最神奇的地方,就在于它的挂起和恢复功能。

    • 在 IntelliJ 中,挂起函数会有一个特殊的箭头标记,调用挂起函数的位置叫做挂起点
    • 表面上看起来是同步的代码,实际上也涉及到了线程切换
      • 比如 val user = getUserInfo(),其中 = 左边的代码运行在主线程,而 = 右边的代码运行在 IO 线程
      • 每一次从主线程到 IO 线程,都是一次协程挂起,每一次从 IO 线程到主线程,都是一次协程恢复
    • 挂起,只是将程序执行流程转移到了其他线程,主线程不会被阻塞

    挂起函数整体的执行流程:

    那么,Kotlin 协程到底是如何做到一行代码切换两个线程的呢?

    深入理解挂起函数

    suspend 会影响函数的类型

    函数类型除了跟参数返回值接收者相关,还跟 suspend 相关。

    后面会遇到的 @Composable 也可以改变函数的类型

    同一个函数,加上 suspend 修饰以后,它的函数类型会发生改变,并且不能互相赋值。

    fun func1(num: Int): Double = num.toDouble()
    suspend fun func2(num: Int): Double = num.toDouble()
    
    val f1: (Int) -> Double = ::func1
    val f2: suspend (Int) -> Double = ::func2
    
    val f3: (Int) -> Double = ::func2         // 报错 Type mismatch
    val f4: suspend (Int) -> Double = ::func1 // 报错 Type mismatch
    

    suspend 的本质是 Callback

    挂起函数的本质,就是 Callback。

    将上面的挂起函数 suspend fun getUserInfo() 反编译成 Java 后是这样的:

    public static final Object getUserInfo(Continuation $completion) { // Continuation 就是一个 CallBack
      // ...
      return "BoyCoder";
    }
    
    public interface Continuation<in T> {        // Continuation 本质上就是一个带有泛型参数的 CallBack
        public fun resumeWith(result: Result<T>) // 相当于 CallBack 的 onSuccess
    }
    

    虽然我们写出来的挂起函数并没有任何 Callback 的逻辑,但是,当 Kotlin 编译器检测到 suspend 关键字修饰的函数以后,就会自动将挂起函数转换成带有 CallBack 的函数。

    • 从挂起函数转换成 CallBack 函数的过程,叫做 CPS 转换(Continuation-Passing-Style Transformation)
    • 挂起函数在 CPS 转换过后,函数的类型会变成了 (Continuation) -> Any?
    • 所以在 Java 中访问 Kotlin 挂起函数时,会看到它的参数是 Continuation,返回值是 Object

    挂起函数的核心原理

    Continuation 到底是指什么?

    • Continue 是继续的意思,Continuation 则是接下来要做的事情
    • 放到程序中,Continuation 就代表了程序接下来要执行的代码,或者是剩下的代码

    以上面的代码为例,当程序运行 getUserInfo() 这个挂起函数的时候,它的Continuation则是下图红框中的代码:

    这样理解了 Continuation 以后,CPS 也就容易理解了:

    • CPS,就是将程序接下来要执行的代码进行传递的一种模式
    • CPS 转换,就是将原本的同步挂起函数转换成 CallBack 异步代码的过程

    可以看到,当程序执行到 getUserInfo() 的时候,剩下的未执行代码都被一起打包了起来,以 Continuation 的形式,传递给了 getUserInfo() 的 Callback 回调当中

    以上就是 Kotlin 挂起函数的核心原理,它的挂起和恢复,其实也是通过 CPS 转换来实现的。

    总结:协程之所以是非阻塞,是因为它支持挂起和恢复;而挂起和恢复的能力,主要是源自于挂起函数;而挂起函数是由 CPS 实现的,其中的 Continuation,本质上就是 Callback。

    协程与挂起函数的关系

    挂起函数调用条件案例

    先看个简单的例子:

    fun main() {
        getUserInfo() // IDE 会报错:挂起函数只能被协程或其他挂起函数调用
    }
    

    上面直接在 main 中调用挂起函数时,IDE 会报错:

    Suspend function 'getUserInfo' should be called only from a coroutine or another suspend function

    提示信息很清晰:挂起函数只能被协程或其他挂起函数调用

    fun main() = runBlocking {         // 可以在协程中调用挂起函数
        val user = getUserInfo()
    }
    
    suspend fun anotherSuspendFunc() { // 可以在另一个挂起函数中调用挂起函数
        val user = getUserInfo()
    }
    

    挂起函数调用条件分析

    实际上,以上两种方式之间是有共性的,我们看看 runBlocking 的函数签名:

    public actual fun <T> runBlocking(
        context: CoroutineContext,
        block: suspend CoroutineScope.() -> T // 也是一个挂起函数
    ): T {... }
    

    可以看到,因为第二个参数 block 是一个挂起函数的类型,所以,在 block 中也可以调用挂起函数!

    所以说,虽然协程和挂起函数都可以调用挂起函数,但是协程的 Lambda 也是挂起函数。所以,它们本质上都是因为挂起函数可以调用挂起函数

    挂起函数调用条件本质

    那么,为什么挂起函数可以调用挂起函数,而普通函数不能调用挂起函数呢?

    • 挂起函数本身并不支持挂起,所以它没法在普通函数中调用,而它之所以能在挂起函数中调用,是因为挂起函数最终都是在协程中被调用的,是协程提供了挂起函数运行的环境
    • 具体的运行环境,就是 Continuation 还有上下文环境 CoroutineContext
    • 被调用的挂起函数需要传入一个 Continuation,没有被 suspend 修饰的函数是没有 Continuation 参数的,所以被调用的挂起函数没有办法从普通函数中获取一个 Continuation

    站在目前的阶段来看,我们可以认为:

    • 挂起和恢复,是协程的一种底层能力
    • 而挂起函数,是这种底层能力的一种表现形式
    • 通过暴露出来的 suspend 关键字,我们开发者可以在上层,非常方便地使用这种底层能力

    小结

    • 挂起函数可以让我们能够以同步的方式写异步代码。相比回调地狱式的代码,挂起函数写出来的代码可读性更好、扩展性更好、维护性更好,也更不易出错。
    • 要定义挂起函数,只需在普通函数的基础上,增加一个 suspend 关键字。suspend 关键字是会改变函数类型的,suspend (Int) -> Double(Int) -> Double 并不是同一个类型。
    • 由于挂起函数拥有挂起和恢复的能力,因此对于同一行代码来说,= 左右两边的代码可以执行在不同的线程之上。而这一切,都是因为 Kotlin 编译器这个幕后的翻译官在起作用。
    • 挂起函数的本质就是 Callback。只是 Kotlin 用了一个更加高大上的名字 Continuation
    • Kotlin 编译器将 suspend 翻译成 Continuation 的过程,称为 CPS 转换。这里的 Continuation 是代表了,程序接下来要执行的代码,或者是 剩下的代码
    • 挂起函数,只能在协程当中被调用,或者是被其他挂起函数调用。协程中的 block,本质上仍然是挂起函数。
    • 挂起和恢复是协程的一种底层能力,而挂起函数则是一种上层的表现形式。

    2017-02-25

  • 相关阅读:
    onInterceptTouchEvent 与 onTouchEvent 分析与MotionEvent在ViewGroup与View中的分发
    不用windows不会死
    iframe动态改变内嵌页面高度
    算法5-8:矩形相交
    浅析数据库连接池(二)
    Ubuntu下(Linux+Apache+MYSQL+PHP, LAMP)环境搭建
    unix环境高级编程----进程控制wait()
    centos7 通过kvm+vnc 实现远程桌面虚拟化和创建windows、Linux虚拟机
    Android native层动态库注射
    Windows10 下 JAVA JDK版本设置修改操作
  • 原文地址:https://www.cnblogs.com/baiqiantao/p/6442129.html
Copyright © 2020-2023  润新知