异步编程数据并行化

499 阅读5分钟

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战

在执行异步的时候,需要用到任务并行执行,在提到并行的时候,很多人都会想到并发的概念,这两者虽然只有一字之差,但是功能确有者天壤之别

并行

并行是指多个任务在同一时间点发生,并有不同的cou进行处理,不互相抢占资源。通俗易懂就是:每个任务都用不同的CPU来负责执行。 未命名文件 (2).png

并发

并发是指多个任务在同一时间点给人感觉是同时执行,但是油同一个CPU不同的切换来执行,不同的任务之间互相抢夺CPU的资源。通俗易懂就是:多个任务抢夺同一个CPU资源,CPU不停的切换上下文来执行不同的任务。

未命名文件 (3).png

当大量数据处理的时候,数据并行化可以大量缩短任务的执行时间,例如Java8中的Stream流,我们就可以采用parallelStream将并行流转换为串行流,将同一份数据分解成多个部分,然后并行处理,最后再将执行的结果汇总,得到最终的执行结果,极大的加快了执行的速率。 parallelStream流在底层实现的时候,采用的是fork/join分解合并框架来进行实现的。

并行流的使用,并不是并行的速度一定比串行快,并行在不同的情况下不一定比串行执行的快,影响并行流执行性能主要为下面的几个因素:

  1. 数据大小: 输入数据的大小,直接影响了并行处理的性能。因为在并行内部实现中涉及到了fork/join操作,它本身

就存在性能上的开销。因此只有当数据量很大,使用并行处理才有意义

  1. 源数据结构: fork时会对源数据进行分割,数据源的特性直接影响了fork的性能
    • ArrayList、数组或IntStream.range,可分解性最佳, 因为他们都支持随机读取,因此可以被任意分割
    • HashSet、TreeSet,可分解性一般,其虽然可被分解,但 因为其内部数据结构,很难被平均分解
    • LinkedList、Streams.iterate、 BufferedReader.lines,可分解性极差,因为他们长度未 知,无法确定在哪里进行分割
  2. 装箱拆箱: 尽量使用基本数据类型,避免装箱拆箱
  3. CPU核数: fork的产生数量是与可用CPU核数相关,可用的核数越多,获取的性能提升就会越大
  4. 单元处理开销: 花在流中每个元素的时间越长,并行操作带来的性能提升就会越明显

Fork/Join框架

Fork/Join框架是Java7提供的一个用于并行执行任务的框架,把一个大任务分割成若干个小任务,等若干个子任务执行完成后,将子任务的结果汇总后得到大任务结果的框架。Fork Join的运行流程图如下: 未命名文件 (4).png 从流程图可以看出,Fork/Join框架分为两个步骤:

  1. 分割任务: 将大任务分割成若干个子任务,有可能子任务还是很大,就需要不停的往下面分割,直到分割的任务很小才不往下分割。
  2. 执行任务并且合并结果: 分割的子任务分别放在双端队列里,然后启动几个线程分别从双端的队列获取任务并执行。子任务执行完成的结果都统一放在一个队列里,然后又会启动一个线程从结果队列里获取执行的结果,合并结果数据。

但是有个问题:双端的队列,由于数据的不同造成两边的执行效率不一致,有一端的队列任务执行速度很快,另一端的队列任务执行速度比较慢,则会拖慢整个任务的执行速度,为了解决这种问题,Fork/Join框架采用了一种工作窃取算法来解决这种弊端。何为工作窃取算法?看下面的描述:

工作窃取算法: 指某个线程自己的队列任务执行完成后,从其他的队列窃取任务来执行,由于不同的线程访问同一个队列,会造成多个线程获取队列任务的竞争,为了减少窃取任何和被窃取任务之间的竞争,通常会常用双端队列,被窃取任务线程从双端队列的头部拿任务执行,窃取任务的线程永远从双端队列的尾部拿任务执行,这样可以减少互相之间的竞争。 未命名文件 (5).png

工作窃取算法的有点:这样可以充分利用线程资源并行计算,加快执行的效率,又由于采取了双端队列,减少了线程之间的竞争。但是在某些情况下,还是会存在竞争,当队列只有一个任务时,就会造成资源竞争,还有在执行多需要创建多个线程和多个双端队列,会加大资源的开销,这些都是工作窃取算法的缺点。