qcon广州站web 3.0 专题上线,关注基础设施及相关技术,戳此了解
写点什么

jep 428:结构化并发,简化java多线程编程-金马国际

作者:a n m bazlur rahman

  • 2022 年 6 月 17 日
  • 本文字数:2284 字

    阅读完需:约 7 分钟

jep 428,即,已经从 proposed 状态进入到 target 状态。在 project loom 的框架下,这个 jep 提议引入一个库,将在不同线程中运行的多个任务视为原子操作,以此来简化多线程编程。它可以简化错误处理和取消操作,提高可靠性,并增强可观察性。这个 api 仍然在孵化当中。


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


response handle() throws executionexception, interruptedexception {   try (var scope = new structuredtaskscope.shutdownonfailure()) {       future user = scope.fork(() -> finduser());       future 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  user  = executorservice.submit(() -> finduser());   future 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 在上的分享以及的文章。


原文链接


2022 年 6 月 17 日 08:081

评论

发布
暂无评论
发现更多内容

“芯”有灵“蜥” 走进 intel meetup

“芯”有灵“蜥” 走进 intel meetup

网站地图