vlambda博客
学习文章列表

Project Loom: Java 虚拟机的 Fiber 和 Continuations

Project Loom 的使命是让满足当今要求的并发应用程序更加容易的编写、调试、分析和维护。 Java 从第一天开始就提供了线程,它是一种自然,方便的并发构造(不考虑线程间通信的单独问题)。不过现在它正在被不太方便的抽象所取代,因为它们作为 OS 内核线程的当前实现不足以满足现代需求 ,浪费了云中特别有价值的计算资源。 Project Loom 将引入 Fiber 作为由 Java 虚拟机管理的轻量级,高效线程,使开发人员可以使用相同的简单抽象,但具有更好的性能和更低的占用空间。 我们想再次简化并发! Fiber 由两个部分组成 - continuation 和 scheduler。 由于 Java 已经具有 ForkJoinPool 这个出色的 scheduler,因此将通过向 JVM 添加 Continuations 来实现 Fiber。

landon: 这里说线程本身就是为了一个方便的并发抽象实现,但是因为实现问题,导致要采取一些其他并不方便的手段来更好的利用如 CPU 资源。比如异步。所以 Loom 的初衷就是解决这个问题。

Java 虚拟机上编写的很多应用程序都是并发的,比如服务器和数据库之类的程序需要服务许多请求,从而出现并发以及争夺计算资源。Project Loom 旨在显着降低编写高效的并发应用程序的难度,或更确切地说,消除编写并发程序时要在简单和效率之间做折中。

二十多年前,Java 首次发布时最重要的贡献之一就是它易于访问线程和同步原语。 Java 线程(直接使用或通过处理 HTTP 请求的 Java servlet 的使用)提供了一个用于编写并发应用程序的相对简单的抽象。但是如今编写满足当今要求的并发程序的主要困难之一是运行时提供的并发软件单元(线程)无法与域并发单元的规模相匹配,无论是一个用户,一次交易甚至一次操作。即使应用程序并发的单位很粗糙(例如,一个会话,由单个套接字连接表示),服务器也可以处理多达一百万个并发打开的套接字,而 Java 运行时则使用操作系统线程来实现 Java 线程,无法有效处理数千个以上的内容。几个数量级的不匹配会产生很大的影响。

landon:这个主要说的对于并发的应用程序来说,并发规模都很大。但是 Java 虚拟机线程的当前实现方式是无法满足的。即需求和实现不匹配。比如为每一个 http 请求都分配一个线程处理,在当前是不可能的。

程序员被迫要么将域并发单元直接建模为线程,但是这会严重损失单个服务器可以支持的并发规模。要么 使用其他结构在比线程(任务)更细粒度的级别上实现并发, 以及通过编写不阻塞运行线程的异步代码来支持并发。

landon: 这个理解比如要么一个请求一个线程。要么更细力度,比如通过一个线程池去处理所有请求,但同时要注意不能阻塞线程池,这就要求编写异步代码,否则线程池的线程会因为同步阻塞而导致排队失去响应。

近年来,从 JDK 中的异步 NIO,异步 servlet 和许多异步第三方库中,已将许多异步 API 引入 Java 生态系统。创建这些 API 并不是因为它们更容易编写和理解,因为它们实际上更难。不是因为它们更容易调试或分析 - 它们更难(它们甚至不会产生有意义的堆栈跟踪);不是因为它们的结构比同步 API 更好 - 它们的结构却不太优雅;不是因为它们更适合其他语言或与现有代码很好地集成(它们更不合适),而是因为从占用空间和性能的角度来看,Java 并发软件单元(线程)的实现不足。这是一个令人遗憾的情况,一个好的自然的抽象被一个不太自然的抽象所取代,而这种抽象在许多方面总体上更糟,这仅仅是因为这种抽象的运行时性能特性。

landon:许多异步库被引入,根本原因是线程的不足,而并非说明异步的代码更好

尽管使用内核线程作为 Java 线程的实现有一些优点 - 最显着的原因是内核线程支持所有 native 代码,因此在线程中运行的 Java 代码可以调用 native API。但上述缺点实在太大,无法忽略,或者导致难以编写,维护成本高的代码,或者导致大量的计算资源浪费,当代码在云运行时,这尤其昂贵。确实,某些语言和语言运行时成功地提供了轻量级线程实现,其中最著名的是 Erlang 和 Go,并且该功能非常有用且广受欢迎。

该项目的主要目标是添加一个轻量级的线程构造,我们称之为 Fiber,由 Java 运行时管理,可以与现有的重量级,操作系统提供的重量级实现一起使用。就内存占用而言,Fiber 比内核线程轻得多,并且它们之间的任务切换开销几乎为零。可以在单个 JVM 实例中生成数百万个 Fiber,并且程序员几乎可以毫不犹豫地发出同步的阻塞调用,因为阻塞实际上是免费的。除了使并发应用程序更简单和 / 或更具可伸缩性之外,这将使库作者的工作更加轻松,因为不再需要同时提供同步和异步 API 来进行不同的简单性 / 性能折衷。简单性将不会有任何取舍。

landon: blocking will be virtually free。同时库作者不需要同时提供同步和异步 api 进行简单和性能的权衡。

正如我们将看到的,线程不是原子构造,而是由两个关注点组成的:scheduler 和 continuation。我们当前的意图是将这两个问题分开,并在这两个构建块之上实现 Java FIber。尽管 Fiber 是该项目的主要动机,但也要在面对用户的抽象中添加了 continuations,因为 continuations 还有其他用途。如 Python's generators[3]

landon: loom 除了有 Fiber 外,continuations 也被暴露被用户。这里提到了 python 的 generator 的例子,其中 yield 是关键。可参考 Improve Your Python: 'yield' and Generators Explained[4]

Fiber 可以提供一个低级原语,可以在其上实现有趣的编程范例,例如 channels,actors 和 dataflow。尽管将这些用途考虑在内,但设计任何这些较高级的结构并不是本项目的目标,另外也没有为 Fiber 之间的交换信息建议新的编程风格或推荐的模式(例如,共享内存 vs 消息传递)。由于限制线程的内存访问的问题是其他 OpenJDK 项目的主题,并且此问题适用于线程抽象的任何实现,无论是重量级的还是轻量级的,此项目都可能与其他项目有交接。

该项目的目标是向 Java 平台添加轻量级线程构造 - Fiber。下面将讨论此构造可以采用哪种面向用户的形式。目标是允许大多数 Java 代码(含义是 Java 类文件中的代码,不一定用 Java 编程语言编写)不用修改就可以 Fiber 中运行,或进行最少的修改。尽管在某些情况下这是可能的,但该项目并不需要允许从 Java 代码调用的 native 代码在 Fiber 中运行。该项目的目标也不是确保每段代码在 Fiber 中运行都可以享受性能优势。实际上,在 Fiber 中运行时,某些不太适合轻量级线程的代码可能会降低性能。

landon: native 代码不适合在 Fiber 中运行

该项目的一个目标是向 Java 平台添加公共限定的 continuation(或 coroutine)构造。但是,此目标仅次于 Fiber(需要 continuations,这将在后面解释,但是不必将这些 continuations 作为公共 API 公开)。

该项目的一个目标是尝试各种 fiber scheduler,但是,本项目的目的不是在 schduler 设计上进行任何认真的研究,主要是因为我们认为 ForkJoinPool 可以充当非常好的 fiber scheduler。

由于需要向 JVM 添加操作调用堆栈的功能是一定需要的。因此,本项目的目标也是添加更轻量级的结构,该结构将展开堆栈到某个点,然后调用具有给定参数的方法(基本上,是有效的尾调用的概括)。我们将称该功能为 “展开并调用” 或 UAI(unwind-and-invoke)。向 JVM 添加自动尾部调用优化不是该项目的目标。

该项目可能涉及 Java 平台的不同组件,其功能据认为可以分为以下几个方面:

Continuations and UAI 将在 JVM 中完成,并作为非常瘦的 Java API 公开。Fiber 将主要在 Java 的 JDK 库中实现,但可能需要在 JVM 中提供一些支持。利用阻塞线程的 native 代码的 JDK 库需要进行修改,以便能够在 Fiber 中运行。特别是,这意味着更改 java.io 类。使用低级线程同步(特别是 LockSupport 类)的 JDK 库(例如 java.util.concurrent)将需要进行调整以支持 Fiber,但是所需的工作量取决于 Fiber API,并且在任何情况下,都应该很小(因为 Fiber 将非常相似的 API 暴露给线程)。调试器,分析器和其他可维护性服务将需要了解 Fiber,以提供良好的用户体验。这意味着 JFR 和 JVMTI 将需要适应 Fiber,并且可能会添加相关的平台 MBean。At this point we do not foresee a need for a change in the Java language.

该项目尚处于初期阶段,因此一切(包括其范围)都可能发生变化。

由于内核线程和轻量级线程只是同一抽象的不同实现,因此必然会引起术语混淆。本文档将采用以下约定,项目中的每个信函均应遵循:

线程一词仅指抽象(稍后将进行探讨),而从不指特定实现,因此线程可以指该抽象的任何实现,无论是由 OS 还是由运行时完成。当提到特定的实现时,术语 “重量级线程”,“内核线程” 和“ OS 线程”可以互换使用,以表示由操作系统内核提供的线程的实现。术语轻量级线程,用户模式线程和 Fiber 可以互换使用,以表示由语言运行时提供的线程的实现 - 在 Java 平台的情况下为 JVM 和 JDK 库。这些词(至少在这些早期阶段,当 API 设计不清楚时)并不是指特定的 Java 类。大写的单词 Thread 和 Fiber 会引用特定的 Java 类,并且在讨论 API 设计而不是实现时会经常使用。

线程是顺序执行的计算机指令序列。由于我们正在处理的操作可能不仅涉及计算,而且还涉及 IO,定时暂停和同步。通常,导致计算流等待其外部某个事件的指令。线程因此具有以下功能:暂停自身,并在等待事件发生时自动恢复。在线程等待时,它应该让出 CPU 内核,并允许另一个线程运行。

这些功能由两个不同的方面提供。continuation 是顺序执行的指令序列,并且可能会暂停自身(稍后在 “continuations” 一节中将对 continuations 进行更彻底的处理)。调度程序将 continuations 分配给 CPU 核心,将已暂停的 continuations 替换为可以运行的,并确保最终可以将准备恢复的 continuation 分配给 CPU 核心。然后,一个线程需要两个构造:一个 continuation 和一个 scheduler,尽管这两个不一定必须分别作为 API 公开。

同样,至少在这种情况下,线程是基本的抽象,并不意味着任何编程范例。特别是,它们仅指允许程序员编写可以运行和暂停的顺序代码的抽象,而不是指在线程之间共享信息的任何机制,例如共享内存或传递消息。

由于存在两个独立的问题,因此我们可以为每个问题选择不同的实现。当前,Java 平台提供的线程构造是 Thread 类,它是由内核线程实现的。它依靠 OS 来实现 continuation 和 scheduler。

Java 平台公开的 continuation 构造可以与现有的 Java 调度器(例如 ForkJoinPool,ThreadPoolExecutor 或第三方调度程序)结合使用,也可以与为此目的专门优化的调度器结合使用,以实现 Fiber。

还可以在运行时和 OS 之间拆分这两个线程构建块的实现。例如,在 Google 上对 Linux 内核进行的修改(视频,幻灯片)允许用户模式代码接管调度内核线程,因此实质上是依靠 OS 来实现 continuations,而由库来处理调度。这具有用户模式调度提供的好处,同时仍允许 native 代码在此线程实现上运行,但是它仍然存在占用空间相对较大且堆栈无法调整大小的缺点,因此尚不可用。用另一种方式拆分实现 - 由 OS 调度和由运行时进行 continuations - 似乎根本没有好处,因为它结合了两个方面的最坏情况。

但是,为什么用户模式线程在任何方面都比内核线程更好,为什么它们值得吸引人的轻量级称号呢?同样,方便地分别考虑 continuation 和调度器这两个组件。

为了中止计算,需要 continuation 操作以存储整个调用堆栈上下文,或者简单地说就是存储堆栈。为了支持 native 语言,存储堆栈的内存必须是连续的,并保持在相同的内存地址。尽管虚拟内存确实提供了一定的灵活性,但在此类内核 continuation(即堆栈)的轻量化和灵活性方面仍然存在限制。理想情况下,我们希望堆栈根据使用情况而增长和缩小。由于不需要线程的语言运行时实现来支持任意的 native 代码,因此我们可以在存储连续性方面获得更大的灵活性,从而可以减少占用空间。

线程的 OS 实现中更大的问题是调度程序。首先,OS 调度程序以内核模式运行,因此,每当线程阻塞并将控制权返回给调度程序时,就必须进行非廉价的用户 / 内核切换。另外,OS 调度程序被设计为通用的,可以调度许多不同种类的程序线程。但是运行视频编码器的线程的行为与通过网络发出的一个服务请求的行为有很大不同,并且相同的调度算法对于这两者都不是最优的。在服务器上处理交易的线程倾向于呈现某些行为模式,这给通用 OS 调度程序带来了挑战。例如,交易服务线程 A 对请求执行某些操作,然后将数据传递到另一个线程 B 进行进一步处理是一种常见的模式。这要求两个线程之间的切换同步可能涉及锁定或消息队列,但是模式是相同的:A 对某些数据 x 进行操作,将其移交给 B,唤醒 B,然后阻塞直到它从网络或另一个线程收到另一个请求。这种模式是如此普遍,以至于我们可以假设 A 在解除对 B 的阻塞后不久就会阻塞,因此将 x 与 A 调度在同一个内核上将是有益的,因为 x 已经在内核的缓存中了。此外,将 B 添加到核心本地队列不需要任何昂贵的竞争同步。确实,像 ForkJoinPool 这样的窃取工作的调度程序做出了这种精确的假设,因为它将通过运行任务调度的任务添加到本地队列中。但是,OS 内核无法做出这样的假设。据其所知,线程 A 在唤醒 B 后可能要继续运行很长时间,因此它将把最近未阻塞的 B 调度到另一个内核,因此既需要一些同步,同时当 B 访问 x 时又会导致高速缓存命中问题。

landon:这一节讨论了线程实现的两个部分。一个是可以恢复和暂停的 continuations,一个是 scheduler。并从这两面讨论得出结论轻量级或者用户模式下的线程要更好。

因此 Fiber 就是我们所谓的 Java 计划的用户模式线程。本节将列出 Fiber 的要求,并探讨一些设计问题和选项。它并不意味着要详尽,而只是呈现设计的轮廓并提供所涉及的挑战。

就基本功能而言,Fiber 必须与其他线程(轻量级或重量级)同时运行任意一段 Java 代码,并允许用户等待其终止,即加入它们(landon-join 的用法)。显然,必须有类似于 LockSupport 的 park/unpark 的机制来暂停和恢复光纤。我们还希望获得用于监视 / 调试以及状态(挂起 / 运行)等状态的 fiber 堆栈跟踪。简而言之,由于 fiber 是线程,因此它将具有与重量级线程非常相似的 API,由 Thread 类表示。关于 Java 内存模型,fiber 的行为将与 Thread 的当前实现完全相同。虽然将使用 JVM 管理的 continuations 来实现 fiber,但我们可能还希望使其与操作系统的 continuations 兼容,例如 Google 的用户调度的内核线程。

Fiber 具有一些独有的功能:我们希望 fiber 由可插拔的调度程序进行调度(固定在 fiber 的结构上,或者在暂停时可以更改,例如使用将调度程序作为参数的 unpark 方法)。我们希望 fiber 是可序列化的(在单独的部分中讨论)。

通常,由于抽象是相同的,因此 fiber API 与 Thread 几乎相同,并且我们还希望运行到目前为止已在内核线程中运行的代码,而无需进行任何修改或小的修改即可在 fiber 中运行。 这立即提示了两个设计选项:

1.将 fibers 表示为 Fiber 类,并将 Fiber 和线程的通用 API 分解为一个通用的超类型(临时称为 Strand)。 与线程无关的代码将针对 Strand 进行编程,以便如果代码在 fiber 中运行,则 Strand.currentStrand 将返回 fiber。而如果代码在 fiber 中运行,则 Strand.sleep 将挂起 fiber。2.对两种线程(用户模式和内核模式)使用相同的 Thread 类,并在调用 start 之前在构造函数或 setter 中选择一种实现作为动态属性集。

单独的 Fiber 类可能使我们有更多的灵活性来偏离 Thread,但同时也带来了一些挑战。由于用户模式调度程序无法直接访问 CPU 内核,因此将 fiber 分配给内核是通过在某个工作程序内核线程中运行该线程来完成的,因此,至少在将其调度到某个内核时,每个 fiber 都有一个底层内核线程。尽管底层内核线程的身份不是固定的,但 CPU 内核可能会改变,如果调度程序决定将同一个 fiber 调度到另一个辅助内核线程,则 CPU 内核可能会更改。如果调度程序是用 Java 编写的(如我们所愿),则每个 fiber 甚至都有一个基础的 Thread 实例。如果纤程由 Fiber 类表示,则在纤程中运行的代码(例如使用 Thread.currentThread 或 Thread.sleep)将可以访问基础 Thread 实例,这似乎是不可取的。

如果 fibers 由相同的 Thread 类表示,则用户代码将无法访问 fiber 的基础内核线程,这似乎是合理的,但会带来许多影响。首先,这将需要在 JVM 中进行更多工作,而 JVM 将大量使用 Thread 类,并且需要了解可能的 fiber 实现。另一方面,它将限制我们的设计灵活性。在编写调度程序时,它还会创建一些循环性,需要通过将线程分配给线程(内核线程)来实现线程(fiber)。这意味着我们需要公开 fiber 的 continuation(由 Thread 表示)以供调度程序使用。

因为 fibers 是由 Java 调度程序调度的,所以它们不必是 GC roots,因为在任何给定时间,fiber 要么是可运行的(在这种情况下,对它的引用是由其调度程序保留的),要么是阻塞的,在这种情况下,对它的引用是由被阻塞的对象(例如,锁或 IO 队列)持有,以便可以解除阻塞。

另一个相对主要的设计决策涉及线程局部变量。当前,线程本地数据由(Inheritable)ThreadLocal 类表示。我们如何对待 fibers 中的线程局部性?至关重要的是,ThreadLocals 有两种非常不同的用法。一种是将数据与线程上下文相关联。fibers 也可能需要此功能。另一个是通过分条 (stripping) 减少在并发数据结构中的争用。这种用法滥用了 ThreadLocal 作为处理器局部(更准确地说是 CPU 内核局部)构造的近似值。使用 fibers 时,需要将两种不同的用途区分开来,因为现在可能超过数百万个线程(fibers)的 thread-local 根本不是处理器局部数据的良好近似。对线程上下文和线程处理器近似值的更明确处理的要求不仅限于实际的 ThreadLocal 类,还限于将 Thread 实例映射到数据以进行剥离的任何类。如果 fiber 由线程表示,则需要对这种条带化数据结构进行一些更改。在任何情况下,都希望 fibers 必须添加一个明确的 API,以精确或近似地访问处理器身份。

内核线程的一个重要功能是基于时间片的抢占(为简便起见,在此称为强制抢占或强制抢占)。经过一段时间而不会阻塞 IO 或同步的一段时间的内核线程将被强制抢占。乍一看,这似乎是 fibers 的重要设计和实现问题 - 实际上,我们可能会决定支持它; JVM 安全点应该使它变得简单 - 不仅不重要,而且拥有此功能根本没有多大区别(因此最好放弃它)。原因如下:与内核线程不同,fibers 的数量可能非常大(数十万甚至数百万)。如果许多 fibers 需要太多的 CPU 时间以至于经常需要强行抢占它们,那么当线程数超出内核数个数量级时,应用程序的数量级将不足,因此任何调度策略都将无济于事。如果许多 fibers 需要不频繁地运行长时间的计算,那么一个好的调度程序将为 fibers 分配可用的内核(即工作者内核线程),从而解决此问题。如果一些 fibers 需要经常运行长时间的计算,那么最好在重量级线程中运行该代码。尽管不同的线程实现提供了相同的抽象,但有时一种实现优于另一种实现,并且我们的 fibers 在每种情况下都不一定比内核线程更可取。

但是,实际的实现挑战可能是如何使 fibers 和阻塞内核线程的内部 JVM 代码协调一致。示例包括隐藏的代码,例如将类从磁盘加载到面向用户的功能,例如 synchronized 和 Object.wait。由于 fiber 调度程序将许多 fiber 多路复用到一小组工作内核线程上,因此阻塞内核线程可能会使调度程序的大部分可用资源无法使用,因此应避免使用。

在一个极端情况下,将需要使每种情况对 fiber 都是友好的,即仅阻塞 fiber,而不阻塞由 fiber 触发的底层内核线程;另一极端情况况是都可能继续阻塞底层内核线程。在这两者之间,我们可以使某些构造成为 fiber 阻塞,而其他构造则为内核线程阻塞。有充分的理由相信其中许多情况可以保留不变,即内核线程阻塞。例如,类加载仅在启动期间频繁发生,而之后才很少发生,并且如上所述,fiber 调度程序可以轻松地在对这种阻塞情形进行调度。synchronized 许多用途只能在很短的时间内保护内存访问和阻塞—太短了,因此可以完全忽略该问题。我们甚至可能决定保持同步不变,并鼓励那些围绕 IO 访问进行同步并经常阻塞的人更改代码,以使用 juc 构造(这将是 fiber 友好的)如果想在 fiber 中运行代码,对于 Object.wait 的使用(这在现代代码中并不常见),无论如何(或者我们相信目前为止)都使用 j.u.c。

无论如何,阻塞其底层内核线程的 fiber 将触发一些可由 JFR / MBean 监视的系统事件。

fiber 鼓励普通的用法,简单和自然的同步阻塞代码,这会很容易适应现有的异步 API,将它们转变为 fiber 阻塞的 API。 假设一个库为某些长时间运行的操作 foo 公开了此异步 API,该操作返回一个 String:




interface AsyncFoo { public void asyncFoo(FooCompletion callback);}

回调或完成处理接口 FooCompletion 定义如下





interface FooCompletion { void success(String result); void failure(FooException exception);}

我们提供了一个’异步的阻塞 fiber’的结构,看起来可能向这样





























abstract class _AsyncToBlocking<T, E extends Throwable> { private _Fiber f; private T result; private E exception;
protected void _complete(T result) { this.result = result; unpark f }
protected void _fail(E exception) { this.exception = exception; unpark f }
public T run() throws E { this.f = current fiber register(); park if (exception != null) throw exception; return result; }
public T run(_timeout) throws E, TimeoutException { ... }
abstract void register();}

然后,我们可通过首先定义以下类来创建阻塞版本












abstract class AsyncFooToBlocking extends _AsyncToBlocking<String, FooException>  implements FooCompletion { @Override public void success(String result) { _complete(result); } @Override public void failure(FooException exception) { _fail(exception); }}

然后我们用同步的版本包装异步 api










class SyncFoo { AsyncFoo foo = get instance;
String syncFoo() throws FooException { new AsyncFooToBlocking() { @Override protected void register() { foo.asyncFoo(this); } }.run(); }}

我们可以为常见的异步类(例如 CompletableFuture)包括此类现成的集成。

landon-fiber 调度器会将 fiber 复用到内核线程,所以应避免阻塞内核线程。另外一个问题是如何将现有代码和 fiber 做协调。

代码的示例是指原有一个耗时 foo 的接口返回 string,现在改造为异步接口传一个 callback. 那么可以通过 fiber 直接改造,即在 fiber 中执行耗时,然后 park。在执行完毕后,unpark。

向 Java 平台添加 continuations 的动机是为了实现 fibers,但是 continuations 还有其他有趣的用途,因此,作为公共 API 提供 continuations 是该项目的第二个目标。然而,那些其他用途的效用预计将远低于 fibers。实际上,continuations 不会在 fibers 之上增加表达性(即,可以在 fibers 之上实现连续性 continuations)。

在本文档中以及 Project Loom 中的所有地方,continuation 一词都表示限定的延续(有时也称为 coroutine)。在这里,我们将带限定的 continuations 视为可以暂停(自身)和继续(由调用方继续)的顺序代码。有些人可能更熟悉将 continuations 视为代表计算的 “其余” 或“未来”的对象(通常是子协程)的观点。两者描述的是同一件事:暂停的 continuations,是一个对象,当恢复或 “调用” 该对象时,它将执行其余的一些计算。

限定的是 continuations 具有入口点(如线程)的顺序子程序,我们将其简称为入口点(在 Scheme 中,这是复位点),该入口点可能会在某个点挂起或放弃执行, 我们将其称为挂起点或避让点(Scheme 中的转移点)。 当限制的 continuations 挂起时,控制权从 continuations 之外传递,当恢复继续时,控制权返回到最后的避让点,执行上下文直到入口点都完好无损。 呈现限定 continuations 的方法有很多,但是对于 Java 程序员来说,以下粗略的伪代码会最好地解释它:


















foo() { // (2) ...  bar() ...}
bar() { ... suspend // (3) ... // (5)}
main() { c = continuation(foo) // (0) c.continue() // (1) c.continue() // (4)}

一个 continuation 在 0 创建,其入口点是 foo。然后调用 1,将控制权传递给 continuation 2 的入口点,然后继续执行直到 bar 子协程的下一个挂起点 3,然后在该点返回调用 1。当再次调用 continuation 4 时,控制点返回到之前的挂起点 5 后的行。

此处讨论的 continuations 是 “有栈”,因为该 continuation 可能会在调用堆栈的任何嵌套深度处阻塞(在我们的示例中,在作为入口点的 foo 调用的 bar 函数中)。相比之下,无栈 continuation 只能在与入口点相同的子协程中挂起。同样,此处讨论的 continuations 是不可重入的,这意味着对该 continuation 的任何调用都可能会更改“当前” 暂停点。换句话说,continuation 对象是有状态的。

实现 continuations(乃至整个项目)的主要技术任务是为 HotSpot 添加捕获,存储和恢复调用栈的能力,而不是将其作为内核线程的一部分。 JNI 堆栈框架可能不受支持。

由于 continuations 是 fibers 的基础,因此如果 continuations 以公共 API 的形式公开,我们将需要支持嵌套的 continuations,这意味着在 continuation 内部运行的代码必须不仅能够暂停 continuation 本身,而且还可以暂停封闭的 continuation(例如, 挂起封闭的 fiber)。 例如,延 continuations 的常见用途是生成器的实现。 生成器公开了一个迭代器,并且在生成器内部运行的代码每次 yield 时都会为迭代器生成另一个值。 因此应该可以这样编写代码:












new _Fiber(() -> { for (Object x : new _Generator(() -> { produce 1 fiber sleep 100ms produce 2 fiber sleep 100ms produce 3 })) { System.out.println("Next: " + x); }})

在文献中,允许这种行为的嵌套 continuations 有时被称为 “带有多个命名提示的限定 continuations”,但我们将它们称为范围 continuations。请参阅此博客文章[5],以讨论有关范围 continuations 的理论表现力的讨论(对于感兴趣的人,continuations 是一种 “一般效果”,即使没有其他方面的纯语言,也可以用于实现任何效果,例如赋值)效果;这就是为什么在某种意义上,continuations 是命令式编程的基本抽象)。

预期在 continuation 运行的代码不会引用该 continuation,并且作用域通常具有一些固定名称(因此,挂起作用域 A 会挂起作用域 A 的最内层包围的 continuation)。但是,挂起点提供了一种机制,可以将信息从代码传递到 continuation 实例,然后再传递回去。当 continuation 挂起时,不会触发包含挂起点的 try / finally 块(即,continuation 运行的代码无法检测到它正在挂起过程中)。

将 continuations 作为独立的 fibers 构造(无论它们是否作为公共 API 公开)实施的原因之一是明确的关注点分离。因此,continuations 不是线程安全的,并且它们的任何操作都不会创建跨线程先行发生的关系。建立内存可见性保证是将 continuations 从一个内核线程迁移到另一个内核线程所必需的,这是 fiber 实现的责任。

下面提供了可能的 API 的粗略概述。 Continuations 是一个非常低级的原语,只会被库作者用来构建更高级的结构(就像 java.util.Stream 实现利用 Spliterator 一样)。 预期使用 contiuations 的类将具有 contiuation 的私有实例,甚至更有可能是其子类的私有实例,并且 contiuation 实例不会直接暴露给构造的使用者。








class _Continuation { public _Continuation(_Scope scope, Runnable target)  public boolean run() public static _Continuation suspend(_Scope scope, Consumer<_Continuation> ccc)
public ? getStackTrace()}

contiuation 终止时,run 方法返回 true;如果暂停,则返回 false。 suspend 方法允许将信息从挂起点传递到 contiuation(使用可以将信息注入给定实例的 ccc 回调),再从 contiuation 传递回挂起点(使用返回值,即 contiuation 实例本身,可以从中查询信息)。

为了演示按照 contiuations 实现光纤的难易程度,这里是代表纤程的_Fiber 类的部分简化实现。 正如您将注意到的那样,大多数代码都会维护 fiber 的状态,以确保不会同时调度 fiber 一次:































































class _Fiber { private final _Continuation cont; private final Executor scheduler; private volatile State state; private final Runnable task;
private enum State { NEW, LEASED, RUNNABLE, PAUSED, DONE; }
public _Fiber(Runnable target, Executor scheduler) { this.scheduler = scheduler; this.cont = new _Continuation(_FIBER_SCOPE, target);
this.state = State.NEW; this.task = () -> { while (!cont.run()) { if (park0()) return; // parking; otherwise, had lease -- continue } state = State.DONE; }; }
public void start() { if (!casState(State.NEW, State.RUNNABLE)) throw new IllegalStateException(); scheduler.execute(task); }
public static void park() { _Continuation.suspend(_FIBER_SCOPE, null); }
private boolean park0() { State st, nst; do { st = state; switch (st) { case LEASED: nst = State.RUNNABLE; break; case RUNNABLE: nst = State.PAUSED; break; default: throw new IllegalStateException(); } } while (!casState(st, nst)); return nst == State.PAUSED; }
public void unpark() { State st, nst; do { State st = state; switch (st) { case LEASED: case RUNNABLE: nst = State.LEASED; break; case PAUSED: nst = State.RUNNABLE; break; default: throw new IllegalStateException(); } } while (!casState(st, nst)); if (nst == State.RUNNABLE) scheduler.execute(task); }
private boolean casState(State oldState, State newState) { ... } }

landon-loom 中的 continuations 为‘有栈协程’

如上所述,像 ForkJoinPools 这样的工作窃取的调度程序特别适合于调度那些经常阻塞并通过 IO 或与其他线程进行通信的线程。 但是,fibers 将具有可插拔的调度程序,并且用户将能够编写自己的调度程序(调度程序的 SPI 可以与 Executor 的 SPI 一样简单)。 根据以前的经验,可以预期异步模式下的 ForkJoinPool 可以用作大多数用途的出色默认 fiber 调度程序,但是我们可能还希望探索一种或两种更简单的设计,例如固定调度程序,该方案始终进行调度给定的 fiber 到特定的内核线程(假定被固定到处理器)。

与 continuations 不同,展开的堆栈帧的内容不会保留,并且不需要任何对象来具体化此构造。 待定

虽然实现此目标的主要动机是使并发更容易 / 更具伸缩性,但是由 Java 运行时实现的线程(运行时对其具有更多控制权)具有其他好处。例如,这样的线程可以在一台机器上暂停和序列化,然后在另一台机器上反序列化并恢复。这在分布式系统中很有用,在分布式系统中,可以通过将代码重定位为更接近其访问的数据来受益,或者在提供功能即服务的云平台中,可以在运行用户代码的机器实例被终止的同时,等待代码等待一些外部时间。然后在另一个实例(可能在不同的物理计算机上)上恢复,从而更好地利用可用资源并降低主机和客户端的成本。这样,fiber 将具有诸如 parkAndSerialize 和 deserializeAndUnpark 之类的方法。

因为我们希望 fibers 可序列化,所以 continuations 也应该可序列化。如果它们是可序列化的,我们也可能使它们可克隆,因为克隆 continuations 的能力实际上会增加表达能力(因为它允许返回到先前的挂起点)。但是,要使 continuations 克隆对此类用途足够有用是一个非常严峻的挑战,因为 Java 代码在堆栈外存储了大量信息,并且为了有用,克隆必须以某种可定制的方式进行 “深度” 处理。

并发的简单性与性能问题相比,fiber 的替代解决方案称为 async / await,已被 C#和 Node.js 采用,并且很可能将由标准 JavaScript 采用。从 async / await 很容易通过 continuations 实现的意义上看,Continuations 和 Fiber 主导着 async / await(实际上,它可以用弱形式的限定 continuations 来实现,即无栈 continuations,它不能捕获整个调用栈,但是仅单个子协程的本地上下文),反之亦然。

尽管实现 async / await 比成熟的 Continuations 和 Fiber 更容易,但是该解决方案远远不足以解决该问题。虽然 async / await 使代码更简单,并使其具有正常的顺序代码外观,但与异步代码一样,它仍然需要对现有代码进行重大更改,在库中进行显式支持,并且无法与同步代码很好地互操作。换句话说,它不能解决所谓的 “有色功能” 问题。

landon-async/await 这种'无栈协程'实现更简单,但是不能解决很多问题

References

[1] 原文: http://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.html
[2] : #project-loom-java%E8%99%9A%E6%8B%9F%E6%9C%BA%E7%9A%84fiber%E5%92%8Ccontinuations
[3] 如 Python's generators: https://wiki.python.org/moin/Generators
[4] Improve Your Python: 'yield' and Generators Explained: https://jeffknupp.com/blog/2013/04/07/improve-your-python-yield-and-generators-explained/
[5] 博客文章: http://blog.paralleluniverse.co/2015/08/07/scoped-continuations/