并行程序设计模式

周明耀

2017-08-21

并行程序设计模式一般有Future模式、Master-Slave模式、保护暂停模式、不变模式、生产者/消费者模式等。

1. Future模式

Future模式有点类似商品订单。比如在进行网上购物时,当看中某一件商品时,就可以提交订单。当订单处理完毕后,便可在家里等待商品送货上门。卖家根据订单从仓库里取货,并配送到客户手上。在大部分情况下,商家对订单的处理并不那么快,有时甚至需要几天时间。而在这段时间内,客户不需要在家里等待,而可以去处理其他事务。

将此例类推到程序设计中,当某一段程序提交了一个请求,期望得到一个答复。但非常不幸的是,服务程序对这个请求的处理可能很慢,比如,这个请求可能是通过互联网、HTTP或者Web Service等并不太高效的方式调用的。在传统的单线程环境下,调用函数是同步的,也就是说它必须等到服务程序返回结果后,才能进行其他处理。而在Future模式下,调用方式改为异步,而原先等待返回的时间段,在主调用函数中,则可用于处理其他事务。虽然call本身仍然需要很长一段时间来处理程序,但是,服务程序不等数据处理完成便立即返回客户端一个伪造的数据(相当于商品的订单,而不是商品本身),实现了Future模式的客户端在拿到这个返回结果后,并不急于对其进行处理,而去调用了其他业务逻辑,充分利用了等待时间,这就是Future模式的核心所在。在完成了其他业务逻辑的处理后,最后再使用返回比较慢的Future数据。这样,在整个调用过程中,就不存在无谓的等待,充分利用了所有的时间片段,从而提高系统的响应速度。

Future模式的主要参与者包括:

  • n Main—系统启动,调用Client发出请求;
  • n Client—返回Data对象,立即返回FutureData,并开启ClientThread线程装配RealData;
  • n Data—返回数据的接口;
  • n FutureData—Future数据,构造很快,但是是一个虚拟的数据,需要装配RealData;
  • n RealData—真实数据,其构造是比较慢的。

代码清单4-18 Future模式示例:

/*
 * Main函数主要负责调用Client发起请求,并使用返回的数据
 */
public class FutureMain{
      public static void main(String[] args){
          Client client = new Client();
          //这里会立即返回,因为得到的是FutureData而不是RealData
          Data data =client.request("name");
          System.out.println("虚拟请求结束");
          try{
              //这里可以用一个Sleep代替对其他业务逻辑的处理,在实际场景中,RealData被创建,节约了等待时间
              Thread.sleep(1000);
          }catch(InterruptedException ex){
              ex.printStackTrace();
          }
          //使用真实数据
          System.out.println(data.getResult);
      }
}

2. Master-Slave模式

Master-Slave(主从)模式主导的系统架构一般由两类线程实现,Master线程负责接收和分发任务(将任务拆成一个个子任务),Worker线程负责处理子任务,每个Worker线程只处理部分任务,所有Worker线程共同完成所有任务的处理。

该模式的好处在于能够将一个大的任务拆分成若干个小的任务,从而交给不同的Worker并行的进行处理,进而提高系统的吞吐量。另外,Client端一旦提交任务后,Master线程完成任务的接收和分发后立即返回,因此对客户端来说,整个过程也是异步进行的。

(1)Master中首先需要维护一个队列Queue,用于接收任务,同时维护一个所有Worker线程的threadMap,以及每个子任务对应的处理结果集resultMap,这里由于涉及多线程同时访问resultMap,因此一般使用JDK中的ConcurrentHashMap实现;
(2)Worker线程实现Runnable或继承Thread,通过Master中的Queue获取拆分后的子任务,并进行业务处理,并将处理结果设置到resultMap中以便Master获取到;
(3)Main入口函数则负责客户端请求的提交(需要先进程拆解),以及通过Master获取各个Worker的结果后进行合并,最后返回给客户端完成处理过程。

目前主流的MapReduce框架、集群框架很多都是采用该种模式架构实现的。

一般的实现思路如下:

3. 保护暂停(Guarded Suspension)模式

所谓“保护暂停”模式,核心思想在于仅当服务进程准备好时,才提供服务。它的好处在于既能保证所有的客户端请求均不丢失,同时也避免了服务器由于同时处理太多的请求而崩溃的现象,有效降低系统的瞬时负载,有助于系统稳定性。

其实这种通过中间加一层Queue做缓冲的模式在工作中用的很多,类似“ClientThread -> Request Queue -> ServerThread”的情况比比皆是,只不过可能实际中我们往往会结合其他方法一起使用,例如:

(1)将ClientThread和ServerThread均为多个,则变为经典的“生产者-消费者”模式;
(2)如果将ServerThread拆为1个Master和多个Worker,则又是上面提到的“Master-Worker”模式;
(3)如果处理的请求需要返回结果,那么又需要和FutureTask结合起来使用(即客户端的请求中需要带上FutureData,并在ServerThread中为FutureData设置上RealData)。

4. 不变模式

并发多线程程序中,当多线程对同一个对象进行读写操作时,为了确保对象数据的一致性和准确性,必须进行同步操作,而这正是对系统性能损失严重的地方。因此,为了提高并发程序的性能,我们可以创建一种不可改变的对象,使用过程中保持不变性。这就是所谓“不变”模式。Java中这种模式用的很广,如String、Boolean、Short、Integer、Long、Byte等。它的好处在于通过回避问题而不是解决问题的态度来处理多线程并发访问控制,但缺点是只适用于对象创建后内部状态和数据不可发生变化的情况。

Java中不变模式的实现很简单,按照OO的思想[1],只需要满足以下几点即可:

(1)将对象的所有属性设为privatefinal的;
(2)通过final修饰class确保类不可被继承;
(3)去掉对象中的所有settXX方法;
(4)有包含所有属性的构造函数用于创建对象。

5. 生产者-消费者模式

生产者线程向内存缓冲区提交任务,消费者线程从内存缓冲区获取任务并进行处理。它的好处在于将生产者线程和消费者线程进行解耦,优化系统整体结构,缓解性能瓶颈对系统性能的影响。

Java中,一般来说使用LinkedBlockingQueue作为上面说的“内存缓冲区”,它是阻塞型BlockingQueue的一种使用Link List的实现,它对头和尾采用两把不同的锁,与ArrayBlockingQueue相比提高了吞吐量,适合于实现“生产者-消费者”模式。实现的大致思路如下:

(1)创建Producer类,实现run方法用于提交任务;
(2)创建Consumer类,实现run方法用于处理任务;
(3)Main函数中建立缓冲区,若干个生产者,若干个消费者,创建线程池并开始使这些线程工作起来。

            

感兴趣的朋友可关注公众号—麦克叔叔每晚十点说,一起交流与学习。

读者评论

相关博文

  • 社区使用反馈专区

    陈晓猛 2016-10-04

    尊敬的博文视点用户您好: 欢迎您访问本站,您在本站点访问过程中遇到任何问题,均可以在本页留言,我们会根据您的意见和建议,对网站进行不断的优化和改进,给您带来更好的访问体验! 同时,您被采纳的意见和建议,管理员也会赠送您相应的积分...

    陈晓猛 2016-10-04
    5700 747 3 7
  • 迎战“双12”!《Unity3D实战核心技术详解》独家预售开启!

    陈晓猛 2016-12-05

    时隔一周,让大家时刻挂念的《Unity3D实战核心技术详解》终于开放预售啦! 这本书不仅满足了很多年轻人的学习欲望,并且与实际开发相结合,能够解决工作中真实遇到的问题。预售期间优惠多多,实在不容错过! Unity 3D实战核心技术详解 ...

    陈晓猛 2016-12-05
    3427 36 0 1
  • czk 2017-07-29
    6277 28 0 1