研究一款 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(....)
关键步骤:
- 调用 work 方法启动任务执行
- WorkerWrapper会检查它所有的依赖项是否已完成,这是通过访问 DependWrapper 来完成的
- 如果所有依赖都满足(或者没有依赖),任务将被提交到 ExecutorService 执行
- 任务执行的结果将由 IWorker 返回,并通过 ICallback 进行处理
- 如果任务执行成功,会有一个成功的回调。如果执行过程中出现异常,WorkerWrapper会调用 fastFail 进行快速失败处理
- 最后,如果有后续的任务依赖于当前任务的完成,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 已经能够覆盖绝大多数场景。
对框架的理解,把握两个要的:依赖遍历算法、依赖执行等待
本篇结束,感谢阅读。