From de4e96a422251af4ff39bcce3af094b612a7ccce Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 3 Feb 2020 02:48:11 +0800 Subject: [PATCH] add kotlin-coroutines-bottom-up to index README --- README.md | 4 ++ kotlin-coroutines-bottom-up/README.md | 62 +++++++++++++-------------- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index fddb951..ae0537b 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,10 @@ Chinese translations for classic IT resources. # 并发 +1. [理解`Kotlin`协程:自底向上的视角](kotlin-coroutines-bottom-up/README.md) + `Kotlin`的协程应该是`Java`生态中最好的协程实现,在生产环境(`Android` / 后端场景)也有比较多实际应用。 + 无论是`Kotlin`语言还是`Kotlin`协程,都非常注重务实与开发者友好,`Kotlin`协程以大家习惯的命令式/过程式的编程方式写出非阻塞的高效并发程序。 + 但并发编程是计算机最复杂的主题之一,即使是用协程的编写方式;再者`Kotlin`协程的友好使用方式,对于使用者理解协程背后的运行机制其实反而是个障碍。而真正的理解协程才能让使用协程做到心中有数避免踩坑。这篇文章自底向上视角的讲解方式,正是有意于正面解决这个问题:如何有效理解`Kotlin`协程运行机制。 1. [`Java` `Fork/Join`框架](a-java-fork-join-framework/README.md) _Doug Lea_ 大神关于`Java 7`引入的他写的`Fork/Join`框架的论文。[反应式编程](https://www.reactivemanifesto.org/zh-CN)(`Reactive Programming`/`RP`)作为一种范式在整个业界正在逐步受到认可和落地,是对过往系统的业务需求理解梳理之后对系统技术设计/架构模式的提升总结。`Java`作为一个成熟平台,对于趋势一向有着稳健的接纳和跟进能力,有着令人惊叹的生命活力:`Java 7`提供了`ForkJoinPool`,支持了`Java 8`提供的`Stream`,另外`Java 8`还提供了`Lambda`(有效地表达和使用`RP`需要`FP`的语言构件和理念);有了前面的这些稳健但不失时机的准备,在`Java 9`中提供了面向`RP`的官方[`Flow API`](https://community.oracle.com/docs/DOC-1006738),实际上是直接把[`Reactive Streams`](http://www.reactive-streams.org/)的接口加在`Java`标准库中,即[`Reactive Streams`规范](https://github.com/reactive-streams/reactive-streams-jvm#specification)转正了。`Reactive Streams`是`RP`的基础核心组件,`Java`提供了`Flow API` 标志着 `RP`完成了由 **集市**式的自由探索阶段 向 **教堂**式的规范统一阶段 的转变。通过上面这些说明,可以看到`ForkJoinPool`的基础重要性。 diff --git a/kotlin-coroutines-bottom-up/README.md b/kotlin-coroutines-bottom-up/README.md index cacc1ef..734f4cc 100644 --- a/kotlin-coroutines-bottom-up/README.md +++ b/kotlin-coroutines-bottom-up/README.md @@ -2,14 +2,14 @@ ## 🍎 译序 -`Kotlin`的协程应该是`Java`生态中最好的协程实现,在生产环境(`Android` / 后端场景)也有比较多实际应用。在移动`Android`开发中,`Google`宣导`Kotlin First`。而在后端开发中,`Spring 5 / Spring Boot`一等公民支持`Kotlin`语言,也[添加了对`Kotlin`协程的支持](https://www.baeldung.com/spring-boot-kotlin-coroutines);相信有后端开发框架王者`Spring`的加持,`Kotlin`语言与`Kotlin`协程的发展普及有着乐观的前景。 +`Kotlin`的协程应该是`Java`生态中最好的协程实现,在生产环境(`Android` / 后端场景)也有比较多实际应用。在移动`Android`开发中,`Google`宣导`Kotlin-First`。而在后端开发中,`Spring 5 / Spring Boot`一等公民支持`Kotlin`语言,也[添加了对`Kotlin`协程的支持](https://www.baeldung.com/spring-boot-kotlin-coroutines);相信有后端开发框架王者`Spring`的加持,`Kotlin`语言与`Kotlin`协程的发展普及有着乐观的前景。 无论是`Kotlin`语言还是`Kotlin`协程,都非常注重务实与开发者友好: -- `Kotlin`语言:相对`Java`,方便简洁(如字符串插值、`data class`、统一原生类型与包装类型、扩展方法、命名参数、默认参数)与安全(如非空类型)。 +- `Kotlin`语言:相对`Java`,方便简洁(如字符串插值、`smart cast`、`data class`、统一原生类型与包装类型、扩展方法、命名参数、默认参数)与安全(如非空类型)。 - `Kotlin`协程:以大家习惯的命令式/过程式的编程方式写出非阻塞的高效并发程序。 -但并发编程是计算机最复杂的问题之一,即使是用协程的编写方式;再者`Kotlin`协程的友好使用方式,对于使用者理解协程背后的运行机制其实反而是个障碍。而真正的理解协程才能让使用协程做到心中有数避免踩坑。这篇文章自底向上视角的讲解方式,正是有意于正面解决这个问题:如何有效理解`Kotlin`协程运行机制。 +但并发编程是计算机最复杂的主题之一,即使是用协程的编写方式;再者`Kotlin`协程的友好使用方式,对于使用者理解协程背后的运行机制其实反而是个障碍。而真正的理解协程才能让使用协程做到心中有数避免踩坑。这篇文章自底向上视角的讲解方式,正是有意于正面解决这个问题:如何有效理解`Kotlin`协程运行机制。 考虑到原文中只给了代码示例但没有给出代码工程,不方便读者直接运行起来以跟着文章自己探索,译者提供了示例代码的可运行代码工程,参见`GitHub`仓库:[`oldratlee/kotlin-coroutines-bottom-up`](https://github.com/oldratlee/kotlin-coroutines-bottom-up)。 @@ -35,10 +35,10 @@ - [3.1 延续传递风格(`Continuation Passing Style`/`CPS`)](#31-%E5%BB%B6%E7%BB%AD%E4%BC%A0%E9%80%92%E9%A3%8E%E6%A0%BCcontinuation-passing-stylecps) - [3.2 挂起还是不挂起 —— 这是一个问题](#32-%E6%8C%82%E8%B5%B7%E8%BF%98%E6%98%AF%E4%B8%8D%E6%8C%82%E8%B5%B7--%E8%BF%99%E6%98%AF%E4%B8%80%E4%B8%AA%E9%97%AE%E9%A2%98) - [3.3 大`switch`语句(`The Big Switch Statement`)和标签](#33-%E5%A4%A7switch%E8%AF%AD%E5%8F%A5the-big-switch-statement%E5%92%8C%E6%A0%87%E7%AD%BE) -- [4. 追踪执行](#4-%E8%BF%BD%E8%B8%AA%E6%89%A7%E8%A1%8C) +- [4. 追踪执行过程](#4-%E8%BF%BD%E8%B8%AA%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B) - [4.1 第三次调用`fetchNewName`的请求 —— 不挂起](#41-%E7%AC%AC%E4%B8%89%E6%AC%A1%E8%B0%83%E7%94%A8fetchnewname%E7%9A%84%E8%AF%B7%E6%B1%82--%E4%B8%8D%E6%8C%82%E8%B5%B7) - [4.2 第三次调用`fetchNewName` —— 挂起](#42-%E7%AC%AC%E4%B8%89%E6%AC%A1%E8%B0%83%E7%94%A8fetchnewname--%E6%8C%82%E8%B5%B7) - - [4.3 总结执行](#43-%E6%80%BB%E7%BB%93%E6%89%A7%E8%A1%8C) + - [4.3 执行过程总结](#43-%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B%E6%80%BB%E7%BB%93) - [5. 结论](#5-%E7%BB%93%E8%AE%BA) @@ -51,27 +51,27 @@ - `Kotlin`实现协程方式是,在编译器中将代码转换为状态机 - 实现协程,`Kotlin`语言只用了一个关键字,剩下的通过库来实现 - `Kotlin`使用延续传递风格(`Continuation Passing Style`/`CPS`)来实现协程 -- 协程使用了调度器(`Dispatchers`),因此在`JavaFX`、`Android`、`Swing`等中使用方式略有不同 +- 协程使用了调度器(`dispatchers`),因此在`JavaFX`、`Android`、`Swing`等中使用方式略有不同 ---------------------------------------- 协程是一个令人着迷的主题,尽管并不是一个新话题。正如[其他地方提到的](https://www.youtube.com/watch?v=dWBsdh0BndM),协程这些年来已经被多次重新发现挖掘出来,通常是作为轻量级线程(`lightweight threading`)或『回调地狱』(`callback hell`)的解决方案。 -最近在`JVM`上,协程已成为反应式编程(`Reactive Programming`)的一种替代方法。诸如[`RxJava`](https://github.com/ReactiveX/RxJava)或[`Project Reactor`](https://projectreactor.io/)之类的框架为客户端提供了一种增量处理传入信息的方式,并且对节流(`throttling`)和并行(`parallelism`)提供了广泛的支持。但是,您必须围绕反应流(`reactive streams`)上的函数式操作(`functional operations`)来重新组织代码,[在很多情况下这样做成本是大于收益的](https://www.youtube.com/watch?v=5TJiTSWktLU)。 +最近在`JVM`上,协程已成为反应式编程(`Reactive Programming`)的一种替代方法。诸如[`RxJava`](https://github.com/ReactiveX/RxJava)或[`Project Reactor`](https://projectreactor.io/)之类的框架为客户端提供了一种增量处理传入信息的方式,并且对节流(`throttling`)和并行(`parallelism`)提供了广泛的支持。但是,必须围绕反应流(`reactive streams`)上的函数式操作(`functional operations`)来重新组织代码,[在很多情况下这样做的成本是高过收益的](https://www.youtube.com/watch?v=5TJiTSWktLU)。 -这就是为什么像`Android`社区会对更简单的替代方案有需求的原因。`Kotlin`语言引入协程作为一个实验功能来满足这个需求,并且经过改进后已成为`Kotlin 1.3`的正式功能。`Kotlin`协程的采用范围已从`UI`开发拓展到服务器端框架(例如[`Spring 5`添加了支持](https://www.baeldung.com/spring-boot-kotlin-coroutines)),甚至是像`Arrow`之类的函数式框架(通过[`Arrow Fx`](https://arrow-kt.io/docs/effects/fx/))。 +这就是为什么像`Android`社区会对更简单的替代方案有需求的原因。`Kotlin`语言引入协程作为一个实验功能来满足这个需求,并且经过改进后已成为`Kotlin 1.3`的正式功能。`Kotlin`协程的采用范围已从`UI`开发拓展到服务器端框架(比如[`Spring 5`添加了支持](https://www.baeldung.com/spring-boot-kotlin-coroutines)),甚至是像`Arrow`之类的函数式框架(通过[`Arrow Fx`](https://arrow-kt.io/docs/effects/fx/))。 ## 1. 协程的理解挑战 -不幸的是理解协程并非易事。尽管`Kotlin`专家进行了许多协程介绍分享,但主要是关于协程是什么(或应如何使用)这方面的见解和介绍。你可能会说协程是并行编程的单子🙂。 +不幸的是理解协程并非易事。尽管有非常多`Kotlin`专家的协程分享,但主要是关于协程是什么(或是协程的用法)这方面的见解和介绍。你可能会说协程是并行编程的单子🙂。 -而要理解协程有挑战的其实是底层实现。在`Kotlin`协程,编译器仅实现 **_`suspend`_** 关键字,其他所有内容都由协程库处理。结果是,`Kotlin`协程非常强大和灵活,但同时也显得用无定形。对于新手来说,这是学习障碍,新手想要的是有一致的指导方针和固定的原则来学习。本文有意于提供这个基础,自底向上地介绍协程。 +而要理解协程有挑战的其实是底层实现。在`Kotlin`协程,编译器仅实现 **_`suspend`_** 关键字,其他所有内容都由协程库处理。结果是,`Kotlin`协程非常强大和灵活,但同时也显得用无定形。对于新手来说,这是学习障碍,而在初学一个东西时就给到固定一致的准则和原则是最好的。本文有意于提供这个基础,自底向上地介绍协程。 ## 2. 示例应用 ### 2.1 示例应用(服务端) -示例应用是一个典型问题:安全有效地对`RESTful`服务进行多次调用。播放[《威利在哪里?》](https://en.wikipedia.org/wiki/Where%27s_Wally%3F)的文本版本 —— 用户要追踪一个连着一个的人名链,直到出现`Waldo`。 +示例应用是一个典型问题:安全有效地对`RESTful`服务进行多次调用。播放[《威利在哪里?》](https://en.wikipedia.org/wiki/Where%27s_Wally%3F)的文字版 —— 用户要追踪一个连着一个的人名链,直到出现`Waldo`。 > 【译注】《威利在哪里?》是一套由英国插画家 _Martin Handford_ 创作的儿童书籍。这个书的目标就是在一张人山人海的图片中找出一个特定的人物 —— 威利。他总是会弄丢东西,如书本、野营设备甚至是他的鞋子,而读者也要帮他在图中找出这些东西来。更多参见 [威利在哪里 - 百度百科](https://baike.baidu.com/item/%E5%A8%81%E5%88%A9%E5%9C%A8%E5%93%AA%E9%87%8C/1019034)、[威利在哪里 - 维基百科](https://zh.wikipedia.org/wiki/%E5%A8%81%E5%88%A9%E5%9C%A8%E5%93%AA%E9%87%8C%EF%BC%9F)。 @@ -115,7 +115,7 @@ fun main() { > 【译注】上面示例代码 完整实现的工程文件: [`ServerMain.kt`](https://github.com/oldratlee/kotlin-coroutines-bottom-up/blob/master/server/src/main/java/com/oldratlee/demo/koroutines_bottom_up/server/ServerMain.kt) -也就是说,我们的客户要完成的操作是执行下面的请求链: +也就是说,用户要完成的操作是执行下面的请求链: ```ruby $ curl http://localhost:8080/wheresWaldo/Mary @@ -184,21 +184,21 @@ fun String.addThreadId() = "$this on thread ${Thread.currentThread().id}" 当用户单击按钮时,会启动一个新的协程,并通过类型为`HttpWaldoFinder`的服务对象访问`RESTful endpoint`。 -`Kotlin`协程存在于`CoroutineScope`之中,`CoroutineScope`关联了表示底层并发模型的`Dispatcher`。并发模型通常是线程池,但可以是其它的。 +`Kotlin`协程存在于`CoroutineScope`之中,`CoroutineScope`关联了表示底层并发模型的`dispatcher`。并发模型通常是线程池,但可以是其它的。 -有哪些`Dispatcher`可用取决于`Kotlin`代码的所运行环境。`Main Dispatcher`对应的是`UI`库的事件处理线程,因此(在`JVM`上)仅在`Android`、`JavaFX`和`Swing`中可用。`Kotlin Native`的协程在开始时完全不支持多线程,[但是这种情况正在改变](https://www.youtube.com/watch?v=oxQ6e1VeH4M)。在服务端,可以自己引入协程,但是缺省就可用情况会越来越多,[比如在`Spring 5`中](https://www.baeldung.com/spring-boot-kotlin-coroutines)。 +有哪些`dispatcher`可用取决于`Kotlin`代码的所运行环境。`Main Dispatcher`对应的是`UI`库的事件处理线程,因此(在`JVM`上)仅在`Android`、`JavaFX`和`Swing`中可用。`Kotlin Native`的协程在开始时完全不支持多线程,[但是这种情况正在改变](https://www.youtube.com/watch?v=oxQ6e1VeH4M)。在服务端,可以自己引入协程,但缺省就可用的情况变得越来越常见,[比如在`Spring 5`中](https://www.baeldung.com/spring-boot-kotlin-coroutines)。 -在开始调用挂起方法(`suspending methods`)之前,必须要有一个协程、一个`CoroutineScope`和一个`Dispatcher`。如果是最开始的调用(如上面的代码所示),可以通过『协程构建器』(`coroutine builder`)函数(如`launch`和`async`)来启动这个过程。 +在开始调用挂起方法(`suspending methods`)之前,必须要有一个协程、一个`CoroutineScope`和一个`dispatcher`。如果是最开始的调用(如上面的代码所示),可以通过『协程构建器』(`coroutine builder`)函数(如`launch`和`async`)来启动这个过程。 -调用协程构建器函数或诸如`withContext`之类的上下文函数总会创建一个新的`CoroutineScope`。在这个上下文中,一个执行任务对应的是由`Job`实例构成的一个层次结构。 +调用协程构建器函数或诸如`withContext`之类的作用域函数(`scoping function`)总会创建一个新的`CoroutineScope`。在这个上下文中,执行的任务(`task`)对应由`Job`实例组成的层次结构。 -任务具有一些有趣的属性,即: +协程的任务具有一些有趣的特性,即: - `Job`在自己完成之前,会等待自己区域中的所有协程完成。 - 取消`Job`导致其所有子`Job`被取消。 - `Job`的失败或取消会传播给他的父`Job`。 -这样的设计是为了避免并发编程中的常见问题,例如在没有终止子任务的情况下终止了父任务。 +这样的设计是为了避免并发编程中的常见问题,比如在没有终止子任务的情况下终止了父任务。 ### 2.3 访问`REST endpoint`的服务 @@ -246,7 +246,7 @@ class HttpWaldoFinder : Controller(), WaldoFinder { ![](images/3-1578570191290.jpg) -当调用挂起函数时,`IntelliJ`会在编辑器窗口左边条用图标提示在协程之间有控制权转移。请注意,如果不切换`Dispatcher`,则调用不一定会导致新协程的创建。当一个挂起函数调用另一个挂起函数时,可以在同一协程中继续执行,实际上,如果处于在同一线程上,这就是我们想要的行为。 +当调用挂起函数时,`IntelliJ`会在编辑器窗口左边条用图标提示在协程之间有控制权转移。请注意,如果不切换`dispatcher`,则调用不一定会导致新协程的创建。当一个挂起函数调用另一个挂起函数时,可以在同一协程中继续执行,实际上,如果处在同一线程上,这就是我们想要的行为。 ![](images/4-1578570189602.jpg) @@ -271,7 +271,7 @@ Sending HTTP Request for Lucy on thread 26 ## 3. 开始探索 -使用`IntelliJ`自带的字节码反汇编工具,可以窥探底层的实际情况。注意,也可以使用`JDK`自带的标准`javap`工具。 +使用`IntelliJ`自带的字节码反汇编工具,可以窥探底层的实际情况。当然你也可以使用`JDK`自带的标准`javap`工具。 ![](images/6-1578570190679.jpg) @@ -315,7 +315,7 @@ $continuation = new ContinuationImpl($completion) { }; ``` -由于所有字段都为`Object`类型,因此如何使用它们并不是很明显。但是随着我们进一步探索,可以看到: +由于所有字段都为`Object`类型,因此如何使用它们并不是很明显。但是随着进一步探索,可以看到: - `L$0`持有对`HttpWaldoFinder`实例的引用。始终有值。 - `L$1`持有`starterName`参数的值。始终有值。 @@ -331,7 +331,7 @@ $continuation = new ContinuationImpl($completion) { `Kotlin`编译器调整了每个挂起函数的返回类型,以便可以返回实际结果或特殊值 _`COROUTINE_SUSPENDED`_。对于后一种情况下当前的协程是被挂起的。这就是挂起函数的返回类型从结果类型更改为`Object`的原因。 -我们的示例应用`wheresWaldo`会重复调用`fetchNewName`。从理论上讲,这些调用中的都可以挂起或不挂起当前的协程。在写`fetchNewName`的时候,我们知道的是挂起总是会发生。但是,如果要理解所生成的代码,我们必须记住,实现需要能够处理所有可能性。 +示例应用`wheresWaldo`会重复调用`fetchNewName`。从理论上讲,这些调用中的都可以挂起或不挂起当前的协程。在写`fetchNewName`的时候,我们知道的是挂起总是会发生。但是,如果要理解所生成的代码,必须记住,实现需要能够处理所有可能性。 ### 3.3 大`switch`语句(`The Big Switch Statement`)和标签 @@ -393,7 +393,7 @@ if (var10000 == var11) { 再强调一次,请记住:生成的代码不能假定所有调用都将挂起,或者所有调用都将继续执行当前协程。必须能够应对任何可能的组合。 -## 4. 追踪执行 +## 4. 追踪执行过程 当执行开始时,延续中`label`字段的值设置的是`0`。`switch`语句中相应分支代码如下: @@ -435,13 +435,13 @@ if (var10000 == var11) { 因为知道`var10000`是函数返回值,所以可以将其转型为正确的类型并存储在本地变量`firstName`中。然后,生成的代码使用变量`secondName`存储连接线程`ID`的结果,然后将其打印出来。 -更新延续中的字段,以添加从服务器检索到的值。请注意,`label`的值现在为2。然后,我们第三次调用`fetchNewName`。 +更新延续中的字段,以添加从服务器检索到的值。请注意,`label`的值现在为2。然后,第三次调用`fetchNewName`。 ### 4.1 第三次调用`fetchNewName`的请求 —— 不挂起 -我们必须再次基于`fetchNewName`返回的值进行选择,如果返回的值是 _`COROUTINE_SUSPENDED`_,那么我们将从当前函数返回。下次调用时,我们将进入`switch`语句的`case 2`分支。 +实现流程必须再次基于`fetchNewName`返回值进行选择,如果返回的值是 _`COROUTINE_SUSPENDED`_,那么将从当前函数返回。下次调用时,将进入`switch`语句的`case 2`分支。 -如果我们继续当前的协程,则执行下面的代码块。正如您所看到的,它与上面的相同,除了我们现在有更多数据要存储在延续中。 +如果继续当前的协程,则执行下面的代码块。从实现流程可以看到,这与前一步相同,除了现在有更多数据要存储在延续中。 **_代码段 4_**:第三次调用`fetchNewName` @@ -481,7 +481,7 @@ case 2: 从延续中提取值到函数的局部变量中。然后用带标签的`break`将执行跳转到上面的代码段4。因此,最终将在同一个地方结束整个协程的执行。 -### 4.3 总结执行 +### 4.3 执行过程总结 现在我们可以重新梳理代码结构,并对每个部分中所做的进行高层的描述: @@ -543,11 +543,11 @@ label48: { 这不是一份容易理解的代码。我们研究了由`Kotlin`编译器生成的字节码所反汇编的`Java`代码。`Kotlin`编译器生成的字节码目标是执行的效率和简约,而不是清晰易读。 -我们可以得出一些有用的结论: +可以得出一些有用的结论: -1. **没有魔法**。当次了解协程时,很容易以为会有一些特殊的『魔法』来把所有东西连接起来。正如我们上面所看到的,生成的代码只使用过程式编程(`Procedural Programming`)的基本构建块,比如条件语句和带有标签的`break`。 -2. **实现是基于延续的**。如`KEEP`提案中所说明,实现函数的挂起和恢复的方式是将函数的状态捕捉到一个对象中。因此,对于每一个挂起函数,编译器会创建一个具有`N`个字段的延续类,其中`N`是函数参数个数加上函数变量个数加上3。后面3个字段持有的是当前对象、最终结果和索引。 -3. **执行始终遵循标准模式**。如果要从挂起中恢复,使用的是延续的`label`字段跳转到`switch`语句的对应分支。在这个分支中,从延续对象中取出所找到的数据,然后用带有标签的`break`语句跳转到如果没有发生挂起所要执行的代码。 +1. **没有魔法**。当初次了解协程时,很容易会以为有一些特殊的『魔法』来把所有东西连接起来。正如我们上面所看到的,生成的代码只使用过程式编程(`Procedural Programming`)的基本构建块,比如条件语句和带有标签的`break`。 +2. **实现是基于延续的**。如`KEEP`提案中所说明,实现函数的挂起和恢复的方式是将函数的状态捕捉到一个对象中。因此,对于每一个挂起函数,编译器会创建一个具有`N`个字段的延续类,其中`N`是函数参数个数加上函数变量个数加上3。后面3个字段分别持有的是当前对象、最终结果和索引。 +3. **执行始终遵循标准模式**。如果要从挂起中恢复,使用延续的`label`字段跳转到`switch`语句里的对应分支。在这个分支中,从延续对象中取出所找到的数据,然后用带有标签的`break`语句跳转到如果没有发生挂起所要执行的代码。 ----------------------------------------