线程
线程是操作系统的内核资源,是 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 开发者社区