Skip to content

loom proposal translate

landon edited this page Apr 6, 2022 · 2 revisions

loom项目提案翻译说明

  1. 原文
  2. 我这边加了自己的一些想法和读书笔记

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

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

目标与范围

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。并从这两面讨论得出结论轻量级或者用户模式下的线程要更好。

Fibers

因此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。

Continuations

向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。请参阅此博客文章,以讨论有关范围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这种'无栈协程'实现更简单,但是不能解决很多问题

Clone this wiki locally