写点什么

eBPF 可观测摄像头带你 5 分钟真正看懂 ForkJoin 是如何“分而治之”?有什么隐藏的坑?

作者:KINDLING
  • 2022-11-22
    浙江
  • 本文字数:1589 字

    阅读完需:约 5 分钟

eBPF可观测摄像头带你5分钟真正看懂ForkJoin是如何“分而治之”?有什么隐藏的坑?

ForkJoin 是一种专为 CPU 密集型任务而生的线程池(比如计算 1~100 亿的和),它能充分利用 CPU 资源,把大任务拆分成众多小的子任务,多线程并行。但是你知道:

  • 子任务的计算量拆分到多少才算合理吗?为什么你用了 ForkJoin 反而降低性能?

  • 大量线程并行,如何规避线程阻塞?

  • 虽知“先 fork 再 join”,但谁负责 join?有什么坑需要注意?

  • ForkJoinPool 的 invoke 和 submit 启动方式竟然还有隐藏的坑你知道吗?


本期眼见为实系列,我们将借助 Kindling 程序摄像头,查看一次请求计算下所有 ForkJoin 线程的工作情况记录和分析,来探究上述问题的答案。

实验 1:分析 ForkJoin 线程工作

首先我基于 ForkJoin 写了一个实验 demo:计算 1~7 的和。下图就是我用程序摄像头捕捉的一次请求计算的 trace 分析。不熟悉 kindling 程序摄像头操作的同学可以参考我们的操作手册:kindling程序摄像头操作手册

页面概览

线程轴放大

(上图中 futex 是指线程在等待,我让线程执行计算后都 sleep 了一段时间,便于放大线程轴观察) 通过每个线程的日志,我们分析发现,ForkJoinPool-worker-1 线程最先拿到了 fork1~7 任务,把它拆解成子任务 1(计算 1~4),子任务 2(计算 5~7),因为我设置的 ForkJoin 的最小任务计算数是 3,所以子任务 1 还需要 fork,而子任务 2 可以直接计算了。最终拆解结果如下图:

我们可以看到,worker-2 线程拆解出来的 1~2 的子任务又丢给了 worker-1 线程计算,此外,最后是由 worker-2 join 了 1~4 的结果,worker-1 join 了 1~7 的结果,即谁 fork 的任务,就由谁来 join 任务。 通过这个实验我们还可以看出,当某个线程执行时间长,需要 join 它结果的线程就会一直等待。如下图红框所示,我让 worker-2join 完 1~4 结果之后 sleep 了 30ms,然后 worker-1 线程这段时间被锁住了,通过点击黄色 lock 区块,我们可以看到堆栈信息,很明显,它在做等待,直到 worker-2 sleep 完成。

这也是 ForkJoin 为什么适合 CPU 密集型计算而不适合 IO 密集型,因为磁盘 IO、网络 IO 的操作特点就是等待,容易造成线程阻塞。另外,我们子任务的拆分要合理,避免造成任务阻塞。那怎么拆分子任务才算合理呢?像我这个实验 demo 的拆解其实是不合理的,需要考虑创建子任务、线程调度等操作都需要消耗时间和内存。官网文档给出的经验是:最小子任务需要计算 100~10000 个基本计算步骤,但是这个相对的最优解应该结合大家的实际业务需求,实践得出。大家就可以借助 Kindling 程序摄像头来做,在设置不同子任务粒度之后,可以观察计算的响应时间,和每个线程的工作实况、耗时分析。

实验 2:ForkJoinPool 的 submit 和 invoke 启动方式

invoke(ForkJoinTask),submit(ForkJoinTask)都有返回值,invoke 的 tasks 会被同步到主线程,但是 submit 是异步执行,需要通过 task.get 实现同步到主线程,什么意思呢?我们做了一个实验,下面是实验 demo:

请求开始打了日志,然后分别用 invoke、submit 启动 ForkJoinPool 测试,请求结束的地方也打了日志,然后 sleep 了 500ms(是为了放大线程轴,一会让大家看得更清楚)。下图是两种情况的测试结果。

submit 启动

invoke 启动

我们可以看到用 submit 启动线程池,主线程和 ForkJoin 线程是异步执行,而 invoke 启动时,主线程同步等待 ForkJoin 线程执行。在等待的时候,主线程被 lock 住(即上图中 4 个问号的黄色高亮区块),点击查看堆栈,确认它确实在等待。

通过这个实验发现,我们在使用 forkJoin 的时候一定要注意 pool 的启动方式,不然就会有隐藏的坑,比如下面这个场景:

  • 先用 forkJoin 给所有订单计算利润,存到数据库订单表里,然后基于订单利润再计算每个销售的订单提成,存到提成表里。

如果开发同学不注意,计算提成的时候取的是订单表的利润,然后用的 submit 启动方式,那可能会造成,开始计算提成的时候,利润还没算好。以上就是本次关于 ForkJoin 分享的内容,你还想通过 Kindling 摄像头观测到什么场景的知识?微服务 RPC 调用机制?Java 锁?Java 分布式?线程安全?线程池调优?Redis 集群机制?欢迎添加微信好友告诉小编~


发布于: 刚刚阅读数: 2
用户头像

KINDLING

关注

还未添加个人签名 2022-11-10 加入

还未添加个人简介

评论

发布
暂无评论
eBPF可观测摄像头带你5分钟真正看懂ForkJoin是如何“分而治之”?有什么隐藏的坑?_Java_KINDLING_InfoQ写作社区