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

2019-12-20 10:48:29 浏览数 (1)

线程

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

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

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

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

协程

什么是CPS呢?

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

代码语言:javascript复制
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风格,则是

代码语言:javascript复制
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 的函数签名如下所示:

代码语言:javascript复制
suspend fun <T> CompletableFuture<T>.await(): T

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

代码语言:javascript复制
fun <T> CompletableFuture<T>.await(continuation: Continuation<T>): Any?

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

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

代码语言:javascript复制
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 是一个标记,返回它的挂起函数表示这个挂起函数会发生事实上的挂起操作。

续体拦截器

代码语言:javascript复制
// 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、函数式编程、编程思想等相关主题。

Kotlin 开发者社区

0 人点赞