两种高效的并发模式:半同步/半异步模式、领导者/追随者模式

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/ZYZMZM_/article/details/98055416


并发编程的目的是为了让程序“同时”执行多个任务。并发编程对计算精密型没有优势,反而由于任务的切换使得效率变低。如果程序是I/O精密型的,比如经常读写文件、访问数据库等,则情况就不同了。由于I/O操作远没有CPU的计算速度快,所以让程序阻塞于I/O操作将浪费大量的CPU时间。如果程序有多个线程,则当前被I/O操作阻塞的线程可主动放弃CPU(或由操作系统来调度),将执行权转给其它线程。这样一来,CPU就可以用来做更加有意义的事情(除非所有线程都同时被I/O操作所阻塞),而不是等待I/O操作完成,因此CPU的利用率显著提升

从实现上说,并发编程主要有多进程和多线程两种方式。

并发模式是指I/O处理单元和多个逻辑单元之间协调完成任务的方法,服务器主要有两种并发编程模式:半同步 / 半异步(half-sync/half-async)模式领导者/追随者(Leader/Followers)模式,下面详述。


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

这里的“同步”和“异步”和我们之前讨论的IO模型中的“同步”“异步”是完全不同的概念。在IO模型中,“同步”和“异步”区分的是内核向应用程序通知的是何种IO事件是就绪事件还是完成事件),以及该由谁来完成IO读写(是应用程序还是内核)

在并发模式中,“同步”指的是程序完全按照代码序列的顺序执行;“异步”指的是程序的执行需要由系统事件来驱动。常见的系统事件包括中断、信号等。

下图描述了并发模式同步读操作(图a)和异步读操作(图b)。

按照同步方式运行的线程为同步线程,按照异步方式运行的为异步线程。异步线程的执行效率高,实时性强,但编写异步方式执行的程序相对复杂,难于调试和扩展,而且不适合于大量的并发。而同步线程虽然效率相对较低,实时性较差,但逻辑简单

因此对于像服务器这种既要求较好的实时性,有要求能够同时处理多个客户请求的程序可以同时使用同步线程和异步线程来实现,即采用半同步/半异步模式

半同步/半异步模式中,同步线程用于处理客户逻辑,异步线程用于处理I/O事件。异步线程监听到客户请求后,就将其封装成请求对象并插入到请求队列中。请求队列将通知某个工作在同步模式的工作线程来读取并处理该请求对象。具体哪个线程处理取决于请求队列的设计。下图为半同步/半异步的工作流程。


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

在服务器程序中,如何结合考虑两种事件处理模式和几种I/O模型,则半同步/半异步模式就存在多种变体,其中有一种变体称为半同步/半反应堆(half-sync/half-reactive)模式,如下图所示。

半同步/半反应堆中,异步线程只有一个,由主线程来充当,它负责监听所有socket上的事件,如果监听socket上有可读事件发生,即有新的连接请求到来,主线程就接受之以得到新的连接socket,然后往epoll内核事件表中注册该socket上的读写事件。如果连接socket上有读写事件发生,即有新的客户请求到来或有数据要发送至客户端,主线程就将该连接socket插入请求队列中。所有工作线程休眠在请求队列中,当任务到来时,它们将通过竞争(比如申请互斥锁)获取任务的接管权。这种竞争机制使得只有空闲的工作线程才有机会来处理新任务,这是很合理的。

在上图的半同步/半反应堆中,主线程插入请求队列的任务是就绪的连接socket,这说明该图所示的半同步/半反应堆模式采用的事件处理模式是Reactor模式:它要求工作线程自己socket上读取客户请求和往socket写入服务器应答。这就是该模式的名称中“half-reactive”的含义。实际上,也可以使用模拟的Proactor模式事件处理,即主线程来完成数据的读写,将数据封装成任务对象插入请求队列,工作线程从请求队列取出任务对象之后可以直接处理,而无须执行读写操作了。(Reactor模式和Reactor模式可以参考此文:服务器两种高效的事件处理模式

半同步/半反应堆存在如下缺点:

  • 主线程和工作线程共享请求队列,主线程往请求队列中添加任务,或者工作线程从请求队列中取出任务,需要对请求队列加锁保护,从而白白浪费耗费CPU时间
  • 每一个工作线程在同一时间只能处理一个客户请求。如果客户数量较多,而工作线程较少,则请求队列中将任务堆积很多任务对象,客户端的响应加速度将会越来越慢,如果通过增加工作线程来解决这一问题,则工作线程的切换也将耗费大量CPU时间。

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

下图描述了一种相对高效的半同步/半异步模式,它的每个工作线程都能同时处理多个客户连接。

主线程只管理监听socket,连接socket由工作线程来管理。当有新的连接到来时,主线程就接受之并将新返回的连接socket派发给某个工作线程,此后该新socket上的任何I/O操作都由被选中的工作线程来处理,直到客户端关闭连接。主线程向工作线程派发socket的最简单的方式,是往它和工作线程之间的管道里写数据。工作线程检测到管道里有数据可读时,就分析是否是一个新的客户连接请求到来。如果是,则把该新socket上的读写事件注册到自己的epoll内核事件表中。

那么每个线程(主线程和工作线程)都维持自己的事件循环,它们各自独立的监听不同的事件。因此在这种高效的半同步/半异步模式中,每个线程都工作在异步模式,所以它并非严格意义上的半同步半异步模式。


领导者/追随者模式(Leader/Followers)

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

领导者/追随者模式包含如下几个组件:

  • 句柄集(HandleSet)
  • 线程集(ThreadSet)
  • 事件处理器(EventHandler)
  • 具体的事件处理器(ConcreteEventHandler)

它们的关系如下图所示:

句柄集
句柄(Handle)表示I/O资源,在Linux下通常就是一个文件描述符。句柄集管理众多句柄,它使用wait_for_event方法监听这些句柄上的I/O事件,并将其中的就绪事件通知给领导者线程。领导者调用绑定到Handle上的事件处理器来处理事件。领导者将Handle和事件处理器绑绑定是通过句柄集的register_handle方法实现的。

线程集
这个组件是所有工作线程(包括领导者线程和追随者线程)的管理者,它负责各线程之间的同步,以及新领导者线程的推选。线程集中的线程在任一时间必处于如下三种状态之一:

  • Leader:领导者线程,负责等待句柄集上的I/O事件。
  • Processing:线程正在处理事件。领导者检测到I/O事件后可以转移至Processing状态处理该事件,并调用promote_new_leader方法推选新领导者;也可以指定其他追随者来处理事件,此时领导者地位不变。当处于Processing状态的线程处理完事件后,如果当前线程集中没有领导者,则它将成为新领导者,否则它直接转为追随者。
  • Follower:线程处于追随者身份,通过调用线程集的join方法等待成为新领导者,也可能被领导者指定来处理新的事件。

下图显示了这三种状态之间的转换关系:

注意:领导者线程推选新的领导者和追随者这两个操作都将修改线程集,因此线程集提供一个Synchronizer来同步这两个操作,以避免竞态条件

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

根据上面的讨论,我们将领导者/追随者模式的工作流程总结于下图:

由于领导者自己监听I/O事件并处理客户请求,因此领导者/追随者模式不需要在线程间传递任何额外数据,也无需像半同步/半反应堆模式那样在线程间同步对请求队列的访问。但是,该模式的明显缺点是仅支持一个事件源集合,因此也无法让每个工作线程独立管理多个客户连接


本文来源:《Linux高性能服务器编程》- 第八章

猜你喜欢

转载自blog.csdn.net/ZYZMZM_/article/details/98055416