结构化编程

1.1 结构化编程的起源和理论基础

结构化编程的起源是 Dijkstra 写了一封信给 CACM 的编辑 Go To Statement Considered Harmful

Dijkstra 是 1972 年图灵奖的获得者,提出了著名的最短路径算法(Dijkstra 算法)和银行家算法(避免死锁问题),并解决了【哲学家就餐问题】。

编程是一项难度很大的活动。一段程序无论复杂与否,都包含了很多的细节信息。如果没有工具的帮助,这些细节的信息是远远超过一个程序员的认知能力范围的。而在一段程序中,哪怕仅仅是一个小细节的错误, 也会造成整个程序出错。

Dijkstra 提出的解决方案是采用数学推导方法。即:程序员可以用代码将一些己证明可用的结构串联起来,只要自行证明这些额外代码是正确的,就可以推导出整个程序的正确性。

Dijkstra 在研究过程中发现了一个问题: goto 语句的某些用法会导致某个模块无法被递归拆分成更小的、可证明的单元,这会导致无法采用分解法来将大型问题进一步拆分成更小的、可证明的部分。

goto 语句的其他用法虽然不会导致这种问题,但是 Dijkstra 意识到它们的实际效果其实和更简单的分支结构 if-then-else 以及循环结构 do-while 是一致的 。 如果代码中只采用了这两类控制结构,则一定可以将程序分解成更小的、可证明的单元。

1.2 结构化编程(Structured Concurrency)简析

结构化编程简单来说就是我们的程序是有结构的,伪代码如下:

 
  1. // statement1
  2. doSomethingPre();
  3. if (case1) {
  4. doSomethingCase1();
  5. } else if (case2) {
  6. doSomethingCase2();
  7. } else {
  8. doSomethingCase3();
  9. }
  10. // statement2
  11. doSomethingAfter();

从上面的伪代码中我们可以清晰的看出, doSomethingPre()doSomethingAfter() 是一定会执行的(假设没有抛出异常),至于中间的条件控制块,当我们不关注其中的具体细节时,可以将其视作一个整体(我们甚至可以将其重构为一个函数/方法 — doSomethingByCases()),我们看到这段代码的第一印象就是:【哦,这是由三条语句顺序组成的代码块】。

然而在非结构化编程中某些函数的调用就不会具备这个特性,示例伪代码如下:

 
  1. print("OPPO is ")
  2. func(true)
  3. print("not")
  4. label HERE
  5. print("the best cellphone")
  6. def func(arg):
  7. if arg :
  8. goto HERE

当我们不知道 func 的具体内容前,这段代码运行的结果是:【OPPO is not the best cellphone】,然而实际的运行的结果却是:【OPPO is the best cellphone】。这就是非结构化编程,无法清晰的看出这段代码的结构,需要每行详细阅读,才能了解具体的逻辑。

一张更经典的图(摘自 Notes on structured concurrency, or: Go statement considered harmful

Fig. 1-1 goto 实际的执行流

2 结构化并发简介

并发编程对于程序员来说是一个无法绕过的话题,本节将简单介绍结构化并发的起源和思想。

2.1 非结构化并发

这里我们使用 jdk19 官方文档的一个例程来描述非结构化并发带来的问题。

 
  1. private ExecutorService esvc = Executors.newCachedThreadPool();
  2. Response handle() throws ExecutionException, InterruptedException {
  3. Future<String> user = esvc.submit(() -> findUser());
  4. Future<Integer> order = esvc.submit(() -> fetchOrder());
  5. String theUser = user.get(); // Join findUser
  6. int theOrder = order.get(); // Join fetchOrder
  7. return new Response(theUser, theOrder);
  8. }

Fig. 2-1 例程逻辑示意图

例程中定义了一个 handle() 方法,执行子任务 findUser() 和 fetchOrder(),并通过阻塞调用 Future.get() 方法获取两个子任务的结果(等待子任务执行完成并返回结果)。

惯性思维里,我们一般认为任何一个子任务失败都会导致整个任务失败。然而在上面的例程中,由于两个子任务是并发执行的,因此每个子任务是否成功执行是相互独立的,而且父任务的失败也不会影响子任务的执行(这里的成功执行可以简单理解为不抛出异常)。我们可以简单分析一下上述例程在什么场景下发生异常,以及异常产生的问题:

  • findUser() 方法抛出异常,在执行到 user.get() 方法时会抛出异常,进而导致 handle() 抛出异常;而 fetchByOrder() 任务将继续执行导致线程泄漏。
  • handle() 在提交任务后失败,并不会中断不能子任务的执行,两个子任务将继续执行,导致线程泄漏。
  • 当 findUser() 执行的时间很长,而 fetchOrder() 这时候已经失败了,handle() 方法不会立即抛出异常结束整个任务,直到 user.get() 执行完成后,执行 order.get() 时才拿到子任务的异常,进而结束整个任务。

线程泄漏轻则浪费计算资源,重则影响其他任务的执行(例如长时间占用数据库连接)。

我们的程序看起来是具有父子任务的逻辑关系,然而运行时并不存在。同时,这些问题排查起来很复杂,thread dump 之类的工具在调用堆栈上并不会显示 handle()、findUser() 和 fetchOrder() 之间的父子逻辑关系。

为了避免上面这种情况,我们执行 handle() 方法时,一般会用 try-catch-finally 的模式包围 handle() 方法,在 catch 中调用 Future.cancel() 方法结束未完成的子任务。除此之外,我们需要改造子任务方法,通过共享标记位、CountDownLatch 等线程同步策略避免 bug,这导致我们的代码看起来非常的复杂,可读性、可维护性大大降低。

2.2 结构化并发

维基百科对于结构化并发的定义如下:

Structured concurrency is a programming paradigm aimed at improving the clarity, quality, and development time of a computer program by using a structured approach to concurrent programming.
结构化并发是一种编程范式,旨在通过使用一种结构化的并发编程方式,提高程序的清晰度、质量,并降低开发时间。

2016年,ZeroMQ 的作者 Martin Sústrik 在 Structured Concurrency 中第一次形提出结构化并发这个概念。2018 年 Nathaniel J. Smith (njs) 在 Python 中实现了这一范式,并在 Notes on structured concurrency, or: Go statement considered harmful一文中进一步阐述了 Structured Concurrency。同时期,Roman Elizarov 也提出了 Structured concurrency,并在 Kotlin 中实现了大家熟知的kotlinx.coroutine。2019年,OpenJDK loom project 也开始引入 structured concurrency,作为其轻量级线程和协程的一部分。

njs 这篇经典文章中,将 go func() (go 语言中启动协程的语法)类比 goto,讲述了 非结构化并发的问题,一张经典的图:

Fig. 2-2 go 是另外一种形式的 goto

结构化并发主要有以下两个核心点:

  • 并发代码块有明确的入口和出口
  • 并发代码块执行过程中产生的子任务在出口之前结束(无论成功/失败/超时)

还是以 Fig 2.1 为例,handle() 创建了两个子任务:findUser() 和 fetchOrder()。handle() 就是一个控制流结构,入口是 handle() 被调用,出口是 handle() 调用结束,子任务 findUser() 和 fetchOrder() 需要在 handle() 结束前完成;当 handle() 完成时,其涉及的资源都要被释放掉,外部调用者无需关心其内部的执行逻辑,也无需担心程序是否失控,只需了解该方法会返回什么样的结果(包括成功、失败或是超时)。这就是所谓的结构化。

3 Java 中的结构化并发

jdk 19 中的结构化并发是一个孵化器 API,主要通过类 jdk.incubator.concurrent.StructuredTaskScope 实现。StructuredTaskScope 构造的对象到底是什么呢?顾名思义,StructuredTaskScope 定义了一个结构化并发作用域(一个具有明确入口和出口的并发代码块),支持将任务分割成几个并发子任务,在各自的线程中执行,子任务将在主任务继续执行前完成。

jdk.incubator.concurrent.StructuredTaskScope 包中的注释原文:

A basic API for structured concurrency. StructuredTaskScope supports cases where a task splits into several concurrent subtasks, to be executed in their own threads, and where the subtasks must complete before the main task continues. A StructuredTaskScope can be used to ensure that the lifetime of a concurrent operation is confined by a syntax block, just like that of a sequential operation in structured programming.

3.1 调试环境

由于 StructuredTaskScope 是一个孵化器 API,因此我们需要添加一些 jvm 参数(本文依旧使用 vscode + java 插件作为 IDE)。

pom 文件

 
  1. <build>
  2. <plugins>
  3. <plugin>
  4. <artifactId>maven-compiler-plugin</artifactId>
  5. <version>3.8.1</version>
  6. <configuration>
  7. <compilerArgs>
  8. <arg>--enable-preview</arg>
  9. <arg>--add-modules jdk.incubator.concurrent</arg>
  10. </compilerArgs>
  11. </configuration>
  12. </plugin>
  13. </plugins>
  14. </build>

launch.json

 
  1. {
  2. "type": "java",
  3. "name": "Launch ${your-main-class-name}",
  4. "request": "launch",
  5. "mainClass": "${your-main-class-name}",
  6. "vmArgs": "--add-modules jdk.incubator.concurrent",
  7. "projectName": "loom-benchmark"
  8. }

3.2 简单使用

第二节中的例程用 StructuredTaskScope 重写一下,并以此为例介绍一下 StructuredTaskScope 使用方法:

 
  1. Response handle() throws ExecutionException, InterruptedException {
  2. try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
  3. // 1. 创建并提交子任务
  4. Future<String> user = scope.fork(() -> findUser());
  5. Future<Integer> order = scope.fork(() -> fetchOrder());
  6. // 2. 等待子任务结束
  7. scope.join();
  8. // 3. 处理结果或异常
  9. scope.throwIfFailed();
  10. return new Response(user.resultNow(), order.resultNow());
  11. } // 4. 关闭 StructuredTaskScope 对象
  12. }

Future.resultNow() 是 jdk 19 新增的 default 方法,立即返回异步结果

 
  1. default V resultNow() {
  2. if (!isDone())
  3. throw new IllegalStateException("Task has not completed");
  4. boolean interrupted = false;
  5. try {
  6. while (true) {
  7. try {
  8. return get();
  9. } catch (InterruptedException e) {
  10. interrupted = true;
  11. } catch (ExecutionException e) {
  12. throw new IllegalStateException("Task completed with exception");
  13. } catch (CancellationException e) {
  14. throw new IllegalStateException("Task was cancelled");
  15. }
  16. }
  17. } finally {
  18. if (interrupted) Thread.currentThread().interrupt();
  19. }
  20. }

可以看到使用 StructuredTaskScope 进行并发编程,主要分为四步:

  1. 创建并提交子任务,fork(Callable task) 方法将创建一个虚拟线程并执行任务,任务返回一个 Future 对象。
  2. 等待子任务结束,join() 方法是一个阻塞调用,将等待所有子任务执行完毕;当然,StructuredTaskScope 也提供有时长限制的方法 joinUtil(Instant deadline)。
  3. 处理结果/异常,根据不同需求来处理结果或是异常。
  4. 结束 StructuredTaskScope 对象,这意味着我们将结束整个任务的调用;由于我们这里使用了 try-with-resource 这种编程模式,所以不需要手动关闭 StructuredTaskScope 对象;当然我们也可以手动结束,调用 shutdown() 方法,或者 close() 方法(AutoCloseable 接口的抽象方法)。

这个编程范式足够结构化,且高度抽象,基本能满足大部分需求。下面我们挑其中几个关键点详细介绍一下:

shutdown 与 close

同样是结束 scope 对象的生命周期,shutdown 与 close 实际上是不一样的,shutdown 可以翻译为中止(使 XX 中途停止),close 翻译为关闭,详细对比如下

  • shutdown() 方法将会中止 scope 中所有的子任务,同时 scope 将不会 start 那些还未执行的任务;shutdown() 只能被主线程或是子任务调用;shutdown() 方法适用于当某个子任务执行完成后,其他任务无需再继续执行的场景。
  • close() 方法首先将结束 scope(通过调用 shutdown 的具体实现 implShutdown()),然后等待所有的子任务都执行完毕;close() 方法只能被主线程调用。

boolean implShutdown() 将中止没有中止的 scope;中止成功返回 true,已经中止的 scope 将返回 false。

scope 的生命周期为:OPEN -> SHUTDOWN -> CLOSED

异常处理

StructuredTaskScope 提供了两种默认的结果/异常处理策略,其 API 为 StructuredTaskScope.ShutdownOnFailure/StructuredTaskScope.ShutdownOnSuccess。

ShutdownOnFailure 捕获到第一个异常时将中止整个 scope,否则将等待所有的子任务执行完成(类似 ExecutorSevice.invokeAll 方法,当且仅当子任务都成功的情况)。提供方法 throwIfFailed(),当任意子任务失败时抛出 ExecutionException;当没有任务执行失败,但是有些任务已经被取消,则抛出 CancellationException,前者可以认为是任务真的执行失败的情况(join),后者则可以认为是某个任务超时的情况(joinUtil)。也提供异常包装的方法 throwIfFailed(Function<Throwable, ? extends X> esf)。

 
  1. public void throwIfFailed() throws ExecutionException {
  2. Future<Object> f = future;
  3. if (f != null) {
  4. if (f.state() == Future.State.FAILED) {
  5. throw new ExecutionException(f.exceptionNow());
  6. } else {
  7. throw new CancellationException();
  8. }
  9. }
  10. }

ShutdownOnSuccess 捕获到第一个成功执行的子任务时中止整个 scope(类似 ExecutorSevice.invokeAny 方法)。提供方法 result() 和异常包装方法 result(Function<Throwable, ? extends X> esf)。

 
  1. public T result() throws ExecutionException {
  2. Future<T> f = future;
  3. if (f == null) {
  4. throw new IllegalStateException("No completed subtasks");
  5. }
  6. return switch (f.state()) {
  7. case SUCCESS -> f.resultNow();
  8. case FAILED -> throw new ExecutionException(f.exceptionNow());
  9. case CANCELLED -> throw new CancellationException();
  10. default -> throw new InternalError("Unexpected state: " + f);
  11. };
  12. }

调用 result 方法就不用定义变量接收每个子任务 scope.fork 的结果,在 scope 中甚至没有一个技术对象,可以然我们更专注于业务逻辑。

 
  1. String handle() throws ExecutionException, InterruptedException {
  2. try (var scope = new StructuredTaskScope.ShutdownOnSuccess()) {
  3. scope.fork(() -> findUserFrom(src1));
  4. scope.fork(() -> findUserFrom(src2));
  5. scope.fork(() -> findUserFrom(src3));
  6. scope.join();
  7. return scope.result();
  8. }
  9. }

3.3 进阶使用

本节将介绍如何创建自定义的结构化并发域。

假设我们有一个商品比价的需求,需要到各个购物网站上抓取商品价格,并返回一个最便宜的商品详情。

 
  1. public static void main(String[] args) throws InterruptedException {
  2. try {
  3. var cheapestGoods = getCheapestGoods();
  4. System.out.println(cheapestGoods);
  5. } catch (RuntimeException e) {
  6. e.printStackTrace();
  7. }
  8. }
  9. public static Goods getGoodsFromWeb(String applier) {
  10. Goods goods = null;
  11. switch (applier) {
  12. case "taobao" -> goods = new Goods("taobao.com", 100);
  13. case "jd" -> goods = new Goods("jd.com", 101);
  14. case "pdd" -> goods = new Goods("pdd.com", 99);
  15. default -> throw new RuntimeException("no such applier");
  16. }
  17. return goods;
  18. }
  19. public static Goods getCheapestGoods() throws InterruptedException {
  20. try (var scope = new GoodsScope()) {
  21. scope.fork(() -> getGoodsFromWeb("taobao"));
  22. scope.fork(() -> getGoodsFromWeb("jd"));
  23. scope.fork(() -> getGoodsFromWeb("pdd"));
  24. scope.fork(() -> getGoodsFromWeb("xxx"));
  25. scope.join();
  26. return scope.cheapestGoods();
  27. }
  28. }
  29. public static class GoodsScope extends StructuredTaskScope<Goods> {
  30. private final BlockingQueue<Goods> goodsList = new ArrayBlockingQueue<>(10);
  31. private final BlockingQueue<Throwable> exceptions = new ArrayBlockingQueue<>(10);
  32. @Override
  33. protected void handleComplete(Future<Goods> future) {
  34. switch (future.state()) {
  35. case RUNNING -> throw new IllegalArgumentException("Task is not completed");
  36. case SUCCESS -> goodsList.add(future.resultNow());
  37. case FAILED -> exceptions.add(future.exceptionNow());
  38. case CANCELLED -> { }
  39. }
  40. }
  41. Goods cheapestGoods() {
  42. return goodsList.stream().min(Comparator.comparing(Goods::getPrice)).orElseThrow(() -> new RuntimeException("no goods"));
  43. }
  44. }
  45. public static class Goods {
  46. private String url;
  47. private Integer price;
  48. // custructor and getter/setter
  49. }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

有马大树

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值