91

CPS(Continuation-Passing-Style, 续体传递风格)

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzA5OTI2MTE3NA%3D%3D&%3Bmid=2658338108&%3Bidx=4&%3Bsn=03cca4bbbebc9dc6077e75ece5f2bd1e
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

线程

线程是操作系统的内核资源,是 CPU 调度的最小单位,所有应用程序的代码都运行于线程之上。

无论是回调,还是 RxJava,又或者是 Future 与 Promise,线程都是我们曾经实现并发与异步的最根本的支撑。在 Java 的 API 中,Thread 类是实现线程最基本的类,每创建一个 Thread 对象,就代表着在操作系统内核启动了一个线程,如果我们阅读 Thread 类的源码,可以发现,它的内部实现是大量的 JNI 调用,因为线程的实现必须由操作系统直接提供支持,如果是在 Android 平台上,我们会发现 Thread 的创建过程中,都会调用 Linux API 中的 pthread_create 函数,这直接说明了 Java 层中的 Thread 和 Linux 系统级别的中的线程是一一对应的。

线程的调用存在以下几个问题;首先,线程阻塞与运行两种状态之间的切换有相当大的开销,在传统的线程调用中,线程状态切换的开销一直是程序中一个较大的优化点,例如 JVM 在运行时会对锁进行各种优化,例如自旋锁,锁粗化,锁消除等。其次,线程并非是一种轻量级资源,大量创建线程是对系统资源的一种消耗,而传统的阻塞调用会导致系统中存在大量因阻塞而不运行的线程,这对系统资源是一种极大的浪费。

协程与线程不同;首先,协程本质上可以认为是运行在线程上的代码块,协程提供的 挂起 操作会使协程暂停执行,而不会导致线程阻塞。其次,协程是一种轻量级资源,即使创建了上千个协程,对于系统来说也不是一种很大的负担,就如同在 Java 创建上千个 Runable 对象也不会造成过大负担一样。通过这样设计,开发者可以极大的提高线程的使用率,用尽量少的线程执行尽量多的任务,其次调用者无需在编程时思考过多的资源浪费问题,可以在每当有异步或并发需求的时候就不假思索的开启协程。

协程

什么是CPS呢?

说的简单点,其实就是函数通过回调传递结果,让我们看看这个例子

class Test {
    public static long plus(int i1, int i2) {
        return i1 + i2;
    }
    public static void main(String[] args) {
        System.out.println(plus(1, 2));
    }
}

这个例子是常规的写法,函数plus的结果通过函数返回值的形式返回并进行后续处理(这里仅仅打印),如果把例子改写成CPS风格,则是

class Test {
    interface Continuation {
        void next(int result);
    }
    public static void plus(int i1, int i2, Continuation continuation) {
        continuation.next(i1 + i2);
    }
    public static void main(String[] args) {
        plus(1, 2, result -> System.out.println(result));
    }
}

很简单吧?这就是CPS风格,函数的结果通过回调来传递, 协程里通过在CPS的Continuation回调里结合状态机流转,来实现协程挂起-恢复的功能.

Kotlin 中被 suspend 修饰符修饰的函数在编译期间会被编译器做特殊处理。而这个特殊处理的第一道工序就是:CPS(续体传递风格)变换,它会改变挂起函数的函数签名。

我们直接展示一个例子:

挂起函数 await 的函数签名如下所示:

suspend fun <T> CompletableFuture<T>.await(): T

在编译期发生 CPS 变换之后:

fun <T> CompletableFuture<T>.await(continuation: Continuation<T>): Any?

编译器对挂起函数的第一个改变就是对函数签名的改变,这种改变被称为 CPS(续体传递风格)变换。

我们看到发生 CPS 变换后的函数多了一个 Continuation<T> 类型的参数,Continuation 这个单词翻译成中文就是续体,它的声明如下:

interface Continuation<in T> {
   val context: CoroutineContext
   fun resumeWith(result: Result<T>)
}

续体是一个较为抽象的概念,简单来说它包装了协程在挂起之后应该继续执行的代码;在编译的过程中,一个完整的协程被分割切块成一个又一个续体。在 await 函数的挂起结束以后,它会调用 continuation 参数的 resumeWith 函数,来恢复执行 await 函数后面的代码。

最后还要提一点,我们看到发生 CPS 变换的函数,返回值类型变成了 Any?,这是因为这个函数在发生变换后,除了要返回它本身的返回值,还要返回一个标记——COROUTINE_SUSPENDED,而这个返回类型事实上是返回类型 T 与 COROUTINE_SUSPENDED 的联合类型,Kotlin 中没有联合类型的概念,貌似也没有加入这种语法的计划,所以只好用最泛化的类型 Any? 来表示,而 COROUTINE_SUSPENDED 是一个标记,返回它的挂起函数表示这个挂起函数会发生事实上的挂起操作。

续体拦截器

// ContinuationInterceptor(续体拦截器)
public interface ContinuationInterceptor : CoroutineContext.Element {

    companion object Key : CoroutineContext.Key<ContinuationInterceptor>

    public fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>

    public fun releaseInterceptedContinuation(continuation: Continuation<*>) {
        /* do nothing by default */
    }

    // Performance optimization for a singleton Key
    public override operator fun <E : CoroutineContext.Element> get(key: CoroutineContext.Key<E>): E? =
        @Suppress("UNCHECKED_CAST")
        if (key === Key) this as E else null

    // Performance optimization to a singleton Key
    public override fun minusKey(key: CoroutineContext.Key<*>): CoroutineContext =
        if (key === Key) EmptyCoroutineContext else this
}

ContinuationInterceptor(续体拦截器),续体拦截器负责拦截恢协程协程在恢复后应执行的代码(即续体)并将其在指定线程或线程池恢复。

在挂起函数的编译中,每个挂起函数都会被编译为一个实现了 Continuation 接口的匿名类,而续体拦截器会拦截真正挂起协程的挂起点的续体。

线程阻塞在协程挂起中的对应特性:

阻塞 挂起 Sychnroized/Lock Mutex BlockQueue Channel sleep delay 线程安全的容器 暂时无

参考资料:

https://www.jianshu.com/p/d23c688feae7

https://www.jianshu.com/p/92be626c594b

Kotlin 开发者社区

国内第一Kotlin 开发者社区公众号,主要分享、交流 Kotlin 编程语言、Spring Boot、Android、React.js/Node.js、函数式编程、编程思想等相关主题。

IF3MZfm.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK