Continuation API

续体 API 使您能够控制程序栈。当一个续体被暂停时,栈会被展开并作为普通 Java 对象复制到堆上。当一个续体被恢复时,这些对象会连同所有必需的元数据一起放回栈上,以在暂停点恢复执行。堆对象可以被序列化,以便在运行相同代码的不同 JVM 中恢复执行(例如,在重启之后)。

用法 #

在编译时将 org.graalvm.espresso:continuations:24.2.0 添加到您的类路径中(它将在运行时自动提供)。续体功能是实验性的,需要使用以下选项显式启用:--experimental-options --java.Continuum=true

查看续体 API 与序列化的用法示例

高级 #

如果您可以将用例建模为发出(或生成)对象流的代码,则可以使用 Generator<T> 类。这提供了一种类似于 Python 生成器且具有方便 API 的功能:将其子类化,实现 generate 方法,并从其中调用 emit 方法。

查看使用 Generator API 的示例.

低级 #

您可以通过向构造函数传递一个实现函数式接口 ContinuationEntryPoint(可以是 lambda 表达式)的对象来创建新的 Continuation。该对象的 start 方法会接收一个 SuspendCapability,让您可以触发暂停。只要代码是通过入口点调用的,您就可以从栈的任何深度执行此操作,并且在调用 suspendresume 之间的所有帧都将被展开并存储在 Continuation 对象中。然后,您可以对其调用 resume() 来首次启动它或从上次暂停点重启。

续体是单线程构造。不涉及其他线程,并且 resume() 方法会阻塞,直到续体成功完成、抛出异常或调用 suspend。您可以使用 isResumable() 检查续体是否可以恢复(例如,如果续体是刚创建的或之前已暂停),并使用 isCompleted() 验证续体是否已完成(通过正常返回,或如果异常逸出)。

Continuation 实现了 Serializable 接口,可以序列化为向后兼容的格式。由于帧可以指向其参数和局部变量中的任何内容,因此 ContinuationSerializable 类提供了静态方法 readObjectExternalwriteObjectExternal,这些方法可用于协调续体相关对象与非 JDK 序列化引擎的序列化。请注意,当指定 --java.Continuum 标志时,所有 lambda 表达式都是可序列化的,但反序列化将需要您的序列化器引擎的特殊支持。

安全 #

实例化的续体是安全的,因为帧记录保留在 VM 内部。

实例化续体是指通过私有 ContinuationImpl.stackFrameHead 字段使记录对 Java 代码可见。目前,实现实例化的唯一途径是通过序列化。

当从已实例化的帧(反实例化)恢复时,VM 只执行最少的检查,并且仅限于防止 VM 崩溃。这些检查的示例包括:

  • 确保恢复只发生在 invoke 字节码上。
  • 确保记录的帧数据与字节码验证器计算的结果一致。
  • 确保记录中的最后一个帧是 ContinuationImpl.suspend

反序列化攻击者提供的续体将允许完全接管 JVM。只恢复您自己持久化的续体!

用例 #

序列化续体使其成为多次执行的,这意味着您可以多次重启续体,从而用不同的输入重复相同的计算。这种探索“并行世界”的能力开辟了许多有趣的用例。

  • 推测执行:CPU 风格的数据推测,用于加速远程高延迟数据存储的使用。参见下文
  • 搜索算法中的回溯:利用续体表示搜索树中的状态,从而可以轻松返回这些状态以进行进一步探索。
  • 实现协程/Yield:通过允许函数在特定点生成并恢复执行,促进协作式多任务处理。
  • Web 请求处理:在 Web 应用程序中,无需依赖全局变量或会话存储即可在 HTTP 请求之间维护状态。
  • 函数式反应式编程 (FRP):在 FRP 系统中管理控制流,其中续体表示数据流的未来状态。
  • 撤销/重做功能:在程序的各个点捕获应用程序状态,以实现撤销或重做操作。将您的应用程序建模为基于续体的 actor,并在每次状态变异后进行序列化。
  • 游戏开发:使用续体而非手工制作的状态机来建模 NPC 和其他交互式实体。例如,monster.walkTo(...) 可以添加到游戏逻辑中,即使步行操作非常慢。这些续体的状态可以包含在游戏存档文件中。
  • 用于模块化的受限续体:在模块化组件中封装控制流,有助于关注点分离和代码可维护性。
  • 用于分布式计算的序列化续体:序列化续体状态,允许分布式系统迁移工作单元。
  • 解析:实现非确定性解析器或解释器,其中多个潜在的解析路径可以同时探索。
  • 自定义控制结构:创建编程语言中不原生支持的新控制结构(例如,循环、异常处理)。
  • 时光旅行调试:在程序的各个点捕获续体,以便在调试会话期间实现“时间回溯”。
  • 实时编程/热代码交换:使用续体在程序部分更新或重新加载时维护程序状态。

推测执行 #

当所依赖的数据尚未到达时,CPU 会尝试猜测分支的方向。这很有用,因为内存速度很慢。当从远程服务器等慢速数据源读取数据时,使用续体可以在更高层面实现相同的技巧。如果您的计算中 CPU 密集型工作与长时间阻塞期交错,这可以加快速度。

当一个续体执行一个会生成值的慢速操作(例如,RPC)时,您可以暂停、序列化并分派请求。与等待结果并调度其他工作(这在基于续体的异步线程实现中是标准做法,例如 Project Loom)不同,您可以选择一个或多个我们认为 RPC 可能返回的值。然后为每个值反序列化续体,为每种可能性分别恢复执行。这种方法将执行过程分叉为多个并行路径。新路径可以再次分叉,形成一棵可能性树。

如果续体遇到一个不受周围框架控制且无法撤销的操作(例如,副作用),它会在该点暂停,并且不会推测性地继续执行。

当远程服务器的结果到达时,序列化续体的推测树可以增量地解析。对应于不再有效路径的序列化续体将被丢弃。一旦收到最终结果,续体要么完成执行,要么继续执行到发生副作用的点之外。

如果服务器协议允许结果链式连接(例如,像 Cap’n’Proto RPCFoundationDB 那样),背靠背的推测调用可以传输到服务器进行本地处理,从而避免往返。

限制 #

在某些特殊情况下,调用 suspend 可能会因 IllegalContinuationStateException 而失败。这些情况包括:

  • 如果调用栈中没有对 resume 的调用。
  • 如果在调用 resumesuspend 之间存在以下任一情况:
    • 持有锁(这可能是通过 MONITORENTER 字节码实现的对象监视器,甚至是 java.util.concurrent.locks.ReentrantLock)。
    • 栈上存在非 Java 帧(这可能是一个 native 方法,甚至是 VM 内联函数)。

此外,目前不支持续体中嵌套续体。

内部实现说明 #

本节仅与 Espresso 开发人员相关。

续体通过为 ContinuationContinuationImpl 类注册的私有内联函数与 VM 交互。

续体通过调用 VM 启动。执行在客户端世界中通过 ContinuationImpl 的私有 run 方法重新出现,然后该方法调用用户提供的入口点。

暂停会抛出一个主机侧异常,该异常由 BytecodeNode 解释器循环捕获。栈帧被复制到一个新的主机侧对象,称为 HostFrameRecord (HFR)。然后该异常被重新抛出。HFR 们通过链表连接在一起。一旦执行到达 ContinuationImpl 的私有 run 方法,HFR 列表就会被附加到 ContinuationImpl 的一个隐藏字段中。然后控制权返回给客户端。

恢复 Continuation 时,整个调用栈需要重新展开。这通过一个与常规调用不同的 CallTarget 发生,并且每个遇到的恢复 bci 都有一个这样的调用目标。

这些调用目标接受一个参数:存储在 Continuation 中的 HostFrameRecord。使用此记录,当前方法的帧被恢复,当前记录与其余记录解除链接(用于 GC 目的),其余记录则传递给下一个方法。所有这些都在一个特殊的调用节点 InvokeContinuableNode 中完成。

调用目标的分离有两个优点:

  • 它不干扰常规调用。
  • 恢复和暂停可以进行部分求值,从而实现快速暂停/恢复周期。

序列化完全在客户端代码中完成,通过让 Continuation 类实现 Serializable 接口。该格式旨在实现格式的向后兼容演进。

进一步阅读 #

联系我们