前言

  我们都知道,并发编程的目的是让程序“同时”执行多个任务,提高效率。当一个程序是计算密集型的时,并发编程并没有优势,反而由于任务的切换时效率降低。但是,当一个程序是IO密集型时,采用并发编程会极大地提高cpu的利用率。因为IO操作的速度远远小于cpu的计算速度,所以让程序阻塞与IO操作上会浪费大量的CPU时间。而并发编程可以让阻塞于IO操作的线程主动放弃CPU,将执行权转移到其他线程。
  
  下面,我们主要来讲一下并发模式,即IO处理单元和多个逻辑单元之间协调完成任务的方法。而在服务器上,主要有两种并发模式。一种是:半同步/半异步模式(half-sync/half-async)和领导者-追随者模式(Leader/Followers)。

简单认识

关于这两个模式有两个很形象的比喻:
半同步/半异步(half-sync/half-async)
  许多餐厅使用 半同步/半异步 模式的变体。例如,餐厅常常雇佣一个领班负责迎接顾客,并在餐厅繁忙时留意给顾客安排桌位,为等待就餐的顾客按序排队是必要的。领班由所有顾客“共享”,不能被任何特定顾客占用太多时间。当顾客在一张桌子入坐后,有一个侍应生专门为这张桌子服务。
领导者/追随者(Leader/Followers)
  在日常生活中,领导者/追随者模式用于管理许多飞机场出租车候车台。在该用例中,出租车扮演“线程”角色,排在第一辆的出租车成为领导者,剩下的出租车成为追随者。同样,到达出租车候车台的乘客构成了必须被多路分解给出租车的事件,一般以先进先出排序。一般来说,如果任何出租车可以为任何顾客服务,该场景就主要相当于非绑定句柄/线程关联。然而,如果仅仅是某些出租车可以为某些乘客服务,该场景就相当于绑定句柄/线程关联。

半同步/半异步模式(half-sync/half-async)

并发模式中,同步与异步的概念

  并发编程中的同步与异步的概念和IO模型中的完全不同。
  在IO模型中,同步和异步区分的是内核向应用进程通知的是就绪事件(同步)还是完成事件(异步);
  在并发模式中,同步指的是,完全按照代码的顺序执行;异步指的是,程序的执行需要系统事件来驱动,比如:中断或信号。
  如下图:a 是同步读    b是异步读
  两种高效的并发模式

原因

  很明显,异步线程(按异步方式运行的线程)的执行效率高,实时性强,但是相对复杂,不适合大量并发;而同步线程(按同步方式运行的线程)虽然讲效率不高,但是逻辑简单。所以,对于服务器来说既要有高并发,实时性还要好的来说,应该同时使用同步线程和异步线程,即半同步/半异步模式。
  这个模式中,高层使用同步I/O模型,简化编程。低层使用异步I/O模型,高效执行。在”复杂度”和”执行效率”之间达到一种平衡。

应用场景

 1 、一个系统中的进程有下面的特征:
  ①、系统必须响应和处理外部异步发生的事件,
  ②、如果为每一个外部资源的事件分派一个独立的线程同步处理I/O,效率很低。
  ③、如果上层的任务以同步方式处理I/O,实现起来简单。
 2、 一个或多个任务必须在单独的控制线程中执行,其它任务可以在多线程中执行:
  ①、上层的任务(如:数据库查询,文件传输)使用同步I/O模型,简化了编写并行程序的难度。
  ②、底层的任务(如网络控制器的中断处理)使用异步I/O模型,提供了执行效率。
一般情况下,上层的任务要比下层的任务多,使用一个简单的层次实现异步处理的复杂性,可以对外隐藏异步处理的细节。另外,同步层次和异步层次任务间的通信使用一个队列来协调。

实现方案

  主要包含三个层次:异步任务层、同步任务层和请求队列层。
  Half-sync/Half-async(半同步/半异步)模式的核心思想是如何将系统中的任务进行恰当的分解,使各个子任务落入合适的层次中。低级的任务或者耗时较短的任务可以安排在异步任务层。而高级的任务或者耗时较长的任务可以安排在同步任务层。而异步任务层和同步任务层这两层之间的协作通过请求队列层进行解耦:请求队列层负责异步任务层和同步任务层之间的数据交换。

工作流程

  ①、同步线程用于处理客户逻辑;
  ②、异步线程用于处理IO事件;
  ③、异步事件监听到客户的请求之后,将其封装成请求对象并插入请求队列中。
  ④、请求队列将通知某个工作在同步模式的工作线程来读取并处理请求对象。

工作流程图

两种高效的并发模式

半同步/ 半反应堆(half-sync / half-reactive)

  在服务器程序中,如果结合考虑两种事件处理模型,和集中IO模型,则半同步/ 半异步模型有多种变种。其中有一种是半同步/半反应堆模型。
  两种高效的并发模式
  异步线程只有一个,由主线程充当,负责监听所有socket事件。如果监听socket上有可读事件发生,指的是新的链接请求到来,那么异步线程接受它,往epoll内核事件表中注册该socket上的读写事件。如果连接socket上有读写事件发生,要么是新的客户请求,要么时有数据要发送给客户端,主线程就把该连接socket插入请求队列。所有的工作线程睡眠在请求队列上,有任务来的时候,空闲线程竞争,获取任务接管权。

缺点
  1 、主线程和工作线程共享一个请求队列,因此当队列中的任务有变更,就需要加锁保护,浪费CPU资源。
  2 、每一个工作线程在同一时间只能处理一个客户请求,对于客户数众多,但是工作任务少的情况下,请求队列很多任务堆积,客户的响应速度越来越慢。若通过增加工作线程来解决的话,工作线程的切换也将浪费大量CPU时间。

高效的半同步/半异步模式

两种高效的并发模式
  主线成只管监听socket,连接socket有工作线程来管理。当有新的连接到来时,主线程就接受并将新返回的连接socket派发给某个工作线程,由该工作线程负责socket上的所有IO操作,知道客户端关闭。
  可见,每个线程都维护着自己的事件循环,各自监听不同的事件。因此,在这种高效的模式中,每个线程都工作在异步模式,并非是严格的半同步/半异步模式。

领导者/追随者模式(Leader-Follower)

工作方式

  领导者/追随者模式是多个工作线程轮流获得事件源集合,轮流监听、分发并处理事件的一种模式。在任意时间点,程序都仅有一个领导者线程,它负责监听IO事件。而其他线程都是追随者,它们休眠在线程池中等待成为新的领导者。当前的领导者如果检测到IO事件,首先要从线程池中推选出新的领导者线程,然后处理IO事件。此时,新的领导者等待新的IO事件,而原来的领导者则处理IO事件,二者实现了并发。

组件

包含如下几个组件:
  句柄集(HandleSet)
  线程集(ThreadSet)
  事件处理器(EventHandler)
  具体的事件处理器(ConcreteEventHandler)
两种高效的并发模式

句柄集

  句柄表示IO资源,linux下通常是文件描述符。句柄集使用wait_for_event方法监听这些句柄上的IO事件,并将其中的就绪事件通知给领导者线程。领导者调用绑定到Handle上的事件处理器来处理事件。绑定是通过句柄集的register_handle方法实现的。

线程集

  所有工作线程的管理者,负责线程同步、推选新领导。线程在任一时间必处于以下三种状态之一:

  Leader:领导者线程,负责等待句柄集上的IO事件。
  Processing:线程正在处理事件。领导者检测到IO事件后可以转移至Processing状态处理该事件,并调用promote_new_leader方法推选新领导者;也可以指定其他追随者来处理事件,此时领导者地位不变。当处于Processing状态的线程处理完事件后,如果当前线程集中没有领导者,则它将成为新领导者,否则它直接转为追随者。
  Follower:线程处于追随者身份,通过调用线程集的join方法等待成为新领导者,也可能被领导者指定来处理新的事件。
  
这三种状态之间的转换关系图:
两种高效的并发模式
注意:领导者推选新领导和追随者等待成为新领导这两个操作都会修改线程集,因此线程集提供一个Synchronizer来同步,避免竟态条件。

事件处理器和具体的事件处理器

  事件处理器通常包含一个或多个回调函数handle_event。这些回调函数用于处理事件对应的业务逻辑。事件处理器在使用前需要被绑定到某个句柄上,当该句柄有事件发生时,领导者就执行绑定的事件处理器的回调函数。具体的事件处理器是事件处理器的派生类。它们重新实现基类的handle_event方法,以处理特定的任务。

领导者/追随者模式的工作流程图

两种高效的并发模式

优缺点

  领导者/追随者模式最大的优点在于,它是自己监听I/O事件并处理客户请求,也就是说从接收到处理都是在同一线程中完成,所以不需要在线程之间传递任何额外的数据,也不用像半同步/半反应堆模式那样在线程间同步对请求队列的访问。但是它也有明显的缺点,就是只支持一种事件源集合,所以导致它不能让每个线程独立的管理多个客户连接。

参考:linxu高性能服务器编程

相关文章: