研究一款 Java 线程编排并行框架-asyncTool

研究一款 Java 线程编排并行框架-asyncTool

📑一、为什么会研究这个框架

最近在 gitee 闲逛,发现了一款可支持线程编排的并行框架 asyncTool。 是京东零售孵化出来的一个项目,目前在 gitee 上属于 GVP 项目。根据其任意编排的特性来了兴趣,因此对其源码进行了阅读研究。这个源码框架也足够轻量,大约就十来个类。

gitee 地址: gitee.com/jd-platform...

下面是对这个框架的介绍

"解决任意的多线程并行、串行、阻塞、依赖、回调的并行框架,可以任意组合各线程的执行顺序,带全链路执行结果回调。多线程编排一站式解决方案"

坦白讲,我心动了。 要是掌握了这个技能,岂不是能在同事面前耀武扬威一把! 因此我又开始了探索之旅......

📝二、这个框架能够做什么

不妨先来看一下它所能解决的场景。

2.1 编排场景

通过 asyncTool 官网,理解其编排场景,如下所示:

框架场景 框架场景

确实有非常多应用场景。

2.2 案例描述

比如,我在一个消息流程中,需要计算三个指标值(如下图所示):

  • 待办任务
  • 已办任务
  • 在办申请

这三个指标值分别来自三个 service 接口;需要将三个值都计算完成,以便计算百分比。

因此这个场景就非常适合使用上面 2 或者 4 的方式来解决。

有了场景输入,接下来我们正式来了解 asyncTool 框架。

2.3 框架特性

特性 描述
任务编排 线程并行、串行、阻塞、依赖、回调的并发框架,自由组合度高,适用于编排场景
回调和默认值 行结果的回调和执行失败后自定义默认值
任务依赖 可以将第一个任务作为下一个任务的入参
设置超时 全组设置超时

下面是官网的特性介绍:

接下来用一个测试用例开启对框架的理解

🎈 三、以测试用例的方式进入 asyncTool的"闺房"

下载源码,开启单测。 seq.TestSequential(这个案例最简单。tips:从容易到复杂)

先通过测试用例,可以快速体验一下框架的魅力。(tips:跑测试来熟悉一个项目是非常不错的一种方式)

执行结果如下,符合预期!
单测是快速理解系统源码最便捷的方式。

继续通过单测来了解依赖、并行等功能。限于篇幅,不一一运行。但是自己阅读时建议一一运行验证。

那么接下来可以进入源码环节了吗,时机还不够。

引用我这篇文章中的图。应该需要从上到下的原则进行。

写代码这件事,迈入第七个年头才有了一些心得(第二章 抽象)

源码的理解都建议按照这种从上到下的方式进行!

📑 四、 自顶向下,从设计到编码

通过文档寻找设计,很遗憾,没有;但是应该要这么做!, 金字塔原理。从精炼到细节。

不管是自己理解,还是让别人理解,这种方式都是一种非常不错的选择。

对整个框架进行了抽象,并进行概括。要理解 asyncTool 可先抽象理解两个核心要点:

  • 依赖遍历算法
  • 依赖执行等待

我认为这是 asyncTool 的精髓。

4.1 依赖遍历算法理解

我们可以将任务按照上图分成不同颜色的模块。模块之间的箭头表示任务之间的依赖

接下来以 TaskC 作为入口任务。我要执行 TaskC,则先执行 TaskA 和 TaskB,然后才能执行 TaskC,最后再执行 TaskD。

思想:按特定的顺序进行的遍历顺序便是依赖关系,被依赖的先执行

因此,不管从 TaskC 作为入口,还是 TaskD 作为入口,它的执行结果都是一致的。(注意:并行节点,执行节点没有具体先后

因此依赖关系构建好,执行的顺序就固定了。

重点:执行顺序就是按照依赖顺序进行遍历。组装好关系就是建立依赖。

除此之外,另外一个核心思想:就是依赖执行等待

4.2 依赖执行等待

能够保证依赖关系能够不出差错顺序进行的关键就是 computureFuture#allOf 。如下图所示,可以保证一个或多个任务执行完毕才继续往下一个节点。

对于 computureFuture 中核心的关键方法其实很多,但是知道两个函数在本框架中就基本够用!

computureFuture#方法 描述
allOf CompletableFuture.allOf(futures).get(remainTime - costTime, TimeUnit.MILLISECONDS); 最为重要的类,等待所有任务结束
runAsync 异步执行任务

注: computureFuture 自身也能实现简单的任务编排

理解上面两个点,再理解 asyncTool 细节

五、💻源码理解

asyncTool 框架并不复杂,核心就几个类

5.1 核心类图

关键类:com.jd.platform.async.wrapper.WorkerWrapper

WorkerWrapper关键属性:

  • id:唯一标识符,用来标识每个 WorkerWrapper 对象。
  • param:工作任务执行时所需的参数。
  • worker:实际执行工作的 IWorker 接口实现。
  • callback:工作完成后回调的 ICallback 接口实现。
  • nextWrappers:依赖于当前 WorkerWrapper 工作完成的下一个 WorkerWrapper 对象列表。
  • dependWrappers:当前 WorkerWrapper 依赖的 WorkerWrapper 对象列表。
  • state:表示当前任务的状态。
  • forParamUseWrappers:存储所有 WorkerWrapper 的映射。
  • workResult:存储工作执行结果的 WorkResult 对象。

5.2 核心流程

入口方法: com.jd.platform.async.wrapper.WorkerWrapper#work(....)

关键步骤:

  1. 调用 work 方法启动任务执行
  2. WorkerWrapper会检查它所有的依赖项是否已完成,这是通过访问 DependWrapper 来完成的
  3. 如果所有依赖都满足(或者没有依赖),任务将被提交到 ExecutorService 执行
  4. 任务执行的结果将由 IWorker 返回,并通过 ICallback 进行处理
  5. 如果任务执行成功,会有一个成功的回调。如果执行过程中出现异常,WorkerWrapper会调用 fastFail 进行快速失败处理
  6. 最后,如果有后续的任务依赖于当前任务的完成,WorkerWrapper 将触发下一个任务的执行

展示了 asyncTool 中 WorkerWrapper 执行任务的基本流程

5.3 主要代码

核心代码一、work() 核心算法

Java 复制代码
private void work(ExecutorService executorService, WorkerWrapper fromWrapper, long remainTime, Map<String, WorkerWrapper> forParamUseWrappers) {
    this.forParamUseWrappers = forParamUseWrappers;
    //将自己放到所有wrapper的集合里去
    forParamUseWrappers.put(id, this);
    long now = SystemClock.now();
    //总的已经超时了,就快速失败,进行下一个
    if (remainTime <= 0) {
        fastFail(INIT, null);
        beginNext(executorService, now, remainTime);
        return;
    }
    //如果自己已经执行过了。
    //可能有多个依赖,其中的一个依赖已经执行完了,并且自己也已开始执行或执行完毕。当另一个依赖执行完毕,又进来该方法时,就不重复处理了
    if (getState() == FINISH || getState() == ERROR) {
        beginNext(executorService, now, remainTime);
        return;
    }

    //如果在执行前需要校验nextWrapper的状态
    if (needCheckNextWrapperResult) {
        //如果自己的next链上有已经出结果或已经开始执行的任务了,自己就不用继续了
        if (!checkNextWrapperResult()) {
            fastFail(INIT, new SkippedException());
            beginNext(executorService, now, remainTime);
            return;
        }
    }

    //如果没有任何依赖,说明自己就是第一批要执行的
    if (dependWrappers == null || dependWrappers.size() == 0) {
        fire();
        beginNext(executorService, now, remainTime);
        return;
    }

    /*如果有前方依赖,存在两种情况
     一种是前面只有一个wrapper。即 A  ->  B
    一种是前面有多个wrapper。A C D ->   B。需要A、C、D都完成了才能轮到B。但是无论是A执行完,还是C执行完,都会去唤醒B。
    所以需要B来做判断,必须A、C、D都完成,自己才能执行 */

    //只有一个依赖
    if (dependWrappers.size() == 1) {
        doDependsOneJob(fromWrapper);
        beginNext(executorService, now, remainTime);
    } else {
        //有多个依赖时
        doDependsJobs(executorService, dependWrappers, fromWrapper, now, remainTime);
    }

}

核心代码二、workerDoJob() 方法执行

Java 复制代码
private WorkResult<V> workerDoJob() {
    //避免重复执行
    if (!checkIsNullResult()) {
        return workResult;
    }
    try {
        //如果已经不是init状态了,说明正在被执行或已执行完毕。这一步很重要,可以保证任务不被重复执行
        if (!compareAndSetState(INIT, WORKING)) {
            return workResult;
        }

        callback.begin();

        //执行耗时操作
        V resultValue = worker.action(param, forParamUseWrappers);

        //如果状态不是在working,说明别的地方已经修改了
        if (!compareAndSetState(WORKING, FINISH)) {
            return workResult;
        }

        workResult.setResultState(ResultState.SUCCESS);
        workResult.setResult(resultValue);
        //回调成功
        callback.result(true, param, workResult);

        return workResult;
    } catch (Exception e) {
        //避免重复回调
        if (!checkIsNullResult()) {
            return workResult;
        }
        fastFail(WORKING, e);
        return workResult;
    }
}

除这两个关键方法外,还有 JDK8 中的computureFuture类

  • computureFuture (能够进行任务编排最核心的类)
  • compareAndSet (CAS,实现无锁的关键)

到这里,asyncTool 的理解就结束了。下面做个简单总结。

✒️ 六、设计不足和注意事项

  • asyncTool 只考虑整体时间,没有针对单个任务做时间判断。当然这么做是复杂的,针对任务编排的总体时间进行控制也是合理的。
  • 默认的线程池是没有边界的。这个对于任务执行时间慢的业务场景一定自定义线程池;同时控制好任务的总体超时时间。
  • 对于普通场景,直接使用 computureFuture 就能够覆盖大多数场景。

如果有复杂场景,可以参考这个框架,但一定要熟知这个框架的特性,比如超时快速失败等场景是否接受; 同时根据业务场景,考虑自定义线程池,以及线程池之间隔离,别共用; 对于普通场景,可以自己基于 computureFuture 封装,api 已经能够覆盖绝大多数场景。

对框架的理解,把握两个要的:依赖遍历算法、依赖执行等待

本篇结束,感谢阅读。

相关推荐
万界星空科技4 分钟前
介绍一款Java开发的商业开源MES系统
java·开发语言·经验分享·科技·5g·开源·制造
振华首席娱记5 分钟前
代码随想录——划分字母区间(Leetcode763)
java·数据结构·算法·leetcode·职场和发展
虫小宝11 分钟前
Spring Boot与Jenkins的集成
spring boot·后端·jenkins
飞翔的佩奇29 分钟前
Java项目:基于SSM框架实现的游戏攻略网站系统分前后台【ssm+B/S架构+源码+数据库+毕业论文+任务书】
java·数据库·spring·游戏·架构·maven·ssm框架
u01040583632 分钟前
构建可扩展的Java Web应用架构
java·前端·架构
长亭外的少年1 小时前
ClickHouse 介绍:深度解析高性能列式数据库的核心优势
java·数据库·clickhouse
tokengo1 小时前
从Java开发者到.NET Core初级工程师学习路线:C#语言基础
java·csharp·新人入门
java6666688881 小时前
深入理解Spring Boot中的配置加载顺序
java·spring boot·后端
春山之外1 小时前
基于IIS的Windows系统Django项目本地部署
后端·python·django·iis·服务器部署
空青7261 小时前
ChatGPT在Java后端开发中的应用与影响
java·开发语言·人工智能·后端·神经网络·机器学习·chatgpt