写点什么

JEP 428:结构化并发,简化 Java 多线程编程

作者:A N M Bazlur Rahman

  • 2022-06-17
  • 本文字数:2284 字

    阅读完需:约 7 分钟

JEP 428:结构化并发,简化Java多线程编程

JEP 428,即结构化并发(孵化器阶段),已经从 Proposed 状态进入到 Target 状态。在 Project Loom 的框架下,这个 JEP 提议引入一个库,将在不同线程中运行的多个任务视为原子操作,以此来简化多线程编程。它可以简化错误处理和取消操作,提高可靠性,并增强可观察性。这个 API 仍然在孵化当中。


开发人员可以使用 StructuredTaskScope 类来组织他们的并发代码,这个类将把一组子任务视为一个单元。子任务通过单独的线程创建,然后连接成一个单元,也可以作为一个单元进行取消。子任务的异常或执行结果将由父任务进行聚合和处理。让我们来看一个例子:


Response handle() throws ExecutionException, InterruptedException {   try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {       Future<String> user = scope.fork(() -> findUser());       Future<Integer> order = scope.fork(() -> fetchOrder());
scope.join(); // 连接 scope.throwIfFailed(); // 抛出错误
// 聚合结果 return new Response(user.resultNow(), order.resultNow()); }}
复制代码


上面的 handle()方法表示服务器应用程序的一个任务。它创建了两个子任务来处理传入的请求。与 ExecutorService.submit()一样,StructuredTaskScope.fork()接受 Callable 作为参数,并返回 Future。与 ExecutorService 不同的是,返回的 Future 不是通过 Future.get()来连接的。这个 API 运行在 JEP 425 之上——虚拟线程(预览阶段),发布目标也为 JDK 19。


上面的例子使用了 StructuredTaskScope API,如果要在 JDK 19 上运行它们,必须添加 jdk.incubator.concurrent 模块,同时要启用预览功能来使用虚拟线程。


使用下面的命令编译上述代码:


javac --release 19 --enable-preview --add-modules jdk.incubator.concurrent Main.java
复制代码


运行程序也需要提供相同的标志:


java --enable-preview --add-modules jdk.incubator.concurrent Main
复制代码


不过,我们也可以使用源代码启动器直接运行它,命令应该是这样的:


java --source 19 --enable-preview --add-modules jdk.incubator.concurrent Main.java
复制代码


jshell 也是可用的,但也需要启用预览功能:


jshell --enable-preview --add-modules jdk.incubator.concurrent
复制代码


结构化并发带来了很多好处。它为调用者方法及其子任务创建了一种父子关系。例如,在上面的例子中,handle()任务是父,它的子任务 findUser()和 fetchOrder()是子。结果,整个代码块变成了原子代码。它通过线程转储中的任务层次结构来提供可观察性。它还可以在错误处理中实现短路,如果其中一个子任务失败,其他未完成的任务将被取消。如果父任务的线程在 join()调用之前或期间被中断,两个分支将在作用域退出时自动取消。这让并发代码的结构变得更加清晰,开发人员现在可以推理和跟踪代码,就好像它们是在单线程环境中运行。


早期的程序流程普遍使用 GOTO 语句来控制,代码十分混乱,这种意大利面条式的代码难以阅读和调试。随着编程范式的成熟,编程社区认识到 GOTO 语句是有害的。1969 年,以《计算机编程的艺术》一书而闻名的计算机科学家 Donald Knuth 表示,没有 GOTO 也可以高效地编写程序。后来,结构化编程的出现解决了所有这些缺点。看一下下面的例子:


Response handle() throws IOException {   String theUser = findUser();   int theOrder = fetchOrder();   return new Response(theUser, theOrder);}
复制代码


上面的代码是结构化代码的一个例子。在单线程环境中,handle()方法被调用时将按顺序执行。fetchOrder()方法不会在 findUser()方法之前启动。如果 findUser()方法失败,下面的方法根本不会启动,handle()方法将隐式失败,这反过来确保了原子操作成功或不成功。它提供了 handle()方法及其子方法之间的父子关系,遵循错误传播的规则,并在运行时提供调用堆栈信息。


然而,这种方法和推理并不适用于我们当前的线程编程模型。例如,如果我们想用 ExecutorService 改写上述的代码,就像这样:


Response handle() throws ExecutionException, InterruptedException {   Future<String>  user  = executorService.submit(() -> findUser());   Future<Integer> order = executorService.submit(() -> fetchOrder());   String theUser  = user.get();   // 连接findUser   int theOrder = order.get();  // 连接fetchOrder   return new Response(theUser, theOrder);}
复制代码


ExecutorService 中的子任务独立运行,可能成功或失败。即使父任务被中断,中断也不会被传播到子任务,因此会造成泄漏。它没有了父关系。由于父任务和子任务将出现在线程转储不相关的线程调用堆栈上,因此调试也变得困难。尽管代码看起来具有逻辑结构,但这种结构只停留在开发人员的头脑中,而不是在执行过程中。所以,它们是非结构化的并发代码。


通过观察非结构化并发代码存在的这些问题,Martin Sústrik 在他的博文中创造了“结构化并发”这个术语,然后 Nathaniel J. Smith 在他关于结构化并发的文章中推广了这个术语。关于结构化并发,Oracle 技术咨询成员、Loom 项目负责人 Ron Pressler 在 InfoQ 的一个播客中说道:


结构化的意思是,如果你生成了什么东西,你必须等待并连接它。这里的“结构”与它在结构化编程中的含义相似。代码的块结构反映了程序的运行时行为。因此,就像结构化编程提供了顺序控制流保证,结构化并发也为并发提供了同样的保证。有兴趣深入了解结构化并发及其背景故事的开发者可以收听 InfoQ 的博客,或者观看 Ron Pressler 在YouTube上的分享以及Inside Java的文章。


原文链接

JEP 428: Structured Concurrency to Simplify Java Multithreaded Programming

2022-06-17 08:084858

评论

发布
暂无评论
发现更多内容
JEP 428:结构化并发,简化Java多线程编程_语言 & 开发_InfoQ精选文章