两种高效的并发模式(半同步/半异步和领导者/追随者)

一、并发编程与并发模式

并发编程主要是为了让程序同时执行多个任务,并发编程对计算精密型没有优势,反而由于任务的切换使得效率变低。如果程序是IO精密型的,则由于IO操作远没有CPU的计算速度快,所以让程序阻塞于IO操作将浪费大量的CPU时间。如果程序有多个线程,则当前被IO操作阻塞的线程可主动放弃CPU,将执行权转给其它线程。(*IO精密型和cpu精密型可以参考此文:CPU-bound(计算密集型) 和I/O bound(I/O密集型)

并发编程主要有多线程和多进程,这里我们先讨论并发模式,并发模式指:IO处理单元和多个逻辑直接协调完成任务的方法。服务器主要有两种并发编程模式:

  • 半同步/半异步模式(half-sync/half-async)
  • 领导者/追随者模式(Leader/Followers)

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

这里的“同步”和“异步”和“IO”的“同步”“异步”是完全不同的概念。在IO模型中,“同步”和“异步”区分的是内核向应用程序通知的是何种IO事件(是就绪事件还是完成事件),以及该由谁来完成IO读写(是应用程序还是内核)。在并发模式中,“同步”指的是程序完全按照代码序列的顺序执行;“异步”指的是程序的执行需要由系统事件来驱动。常见的系统事件包括中断、信号等。

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

图1并发模式同步读(a)和异步读(b)

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

因此对应服务器要求实时性及同时处理多个请求的程序,可以同时使用同步线程和异步线程即采用半同步/半异步模式。同步线程用于处理客户逻辑,异步线程用于处理IO事件。异步线程监听到客户请求后,就将其封装成请求对象并插入到请求队列中。请求队列将通知某个工作在同步模式的工作线程来读取并处理该请求对象。具体哪个线性处理取决于请求队列的设计。下图2为半同步/半异步的工作流程

图2半同步/半异步的工作流程

在半同步/半异步模式可以变体成为半同步/半反应堆(half-sync/half-reactive),如下图3

图3半同步/半反应堆模式

半同步/半反应堆中,异步线程只有一个,即主线程,他负责监听所有事件,有事件发生则将事件插入请求队列中。工作线程休眠在请求队列中,当任务到来时,通过竞争获取任务处理权

在上图3半同步/半反应堆中,主线程插入工作队列的为就绪的连接socket,他要求工作线程自己socket读取数据和往socket写入服务器应答,所有可以看作Reactor模式。实际也可以模拟为Proactor模式,即主线程完成数据的读写,将数据封装成任务对象插入请求队列,工作线程从请求队列取出任务对象处理。(Reactor模式和Reactor模式可以参考此文:服务器两种高效的事件处理模式

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

1、主线程和工作线程共享请求队列,对请求队列的操作需求加锁,耗费CPU时间。

2、每一个工作线程在同一时间只能处理一个客户请求。客户数量多,工作线程少,请求队列任务堆积,响应满,如果添加试图通过增加线程则,由于线程切换导致的CPU时间消耗。

这里我们再介绍一种高效的半同步/半异步模式:每个工作线程都能同时处理多个客户连接。

图4 高效半同步/半异步模式

主线程只管理监听socket,连接socket由工作线程来管理。当有新的连接到来时,主线程就接受之并将新返回的连接socket派发给某个工作线程,此后该socket上的任何IO操作都由被选中的工作线程来处理,直到客户端关闭连接。主线程向工作线程派发socket的最简单的方式,是往它和工作线程之间的管道里写数据。工作线程检测到管道里有数据可读时,就分析是否是一个新的客户连接请求到来。如果是,则把该新socket上的读写事件注册到自己的epoll内核事件表中。
每个线程(主线程和工作线程)都维持自己的事件循环,它们各自独立的监听不同的事件。因此在这种模式中,每个线程都工作在异步模式,所以它并非严格意义上的半同步半异步模式。

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

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

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

关系如下图5

图5 领导者/追随者模式的组件

1、句柄集

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

2、线程集

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

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

这三种状态之间的转换关系图如下图6:

图6 领导者/追随者模式的状态转移

注意,领导者推选新领导和追随者等待成为新领导这两个操作都会修改线程集,因此线程集提供一个Synchronizer来同步。

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

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

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

我们将领导者/追随者模式的工作流程总结如下图7

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

注(本文内容参考 Linux高性能服务器编程——第八章 游双著)

来源: https://my.oschina.net/wangande2014/blog/750147

时间: 2024-08-07 12:36:01

两种高效的并发模式(半同步/半异步和领导者/追随者)的相关文章

两种高效的事件处理模式(Proactor和Reactor)

典型的多线程服务器的线程模型 1. 每个请求创建一个线程,使用阻塞式 I/O 操作 这是最简单的线程模型,1个线程处理1个连接的全部生命周期.该模型的优点在于:这个模型足够简单,它可以实现复杂的业务场景,同时,线程个数是可以远大于CPU个数的.然而,线程个数又不是可以无限增大的,为什么呢?因为线程什么时候执行是由操作系统内核调度算法决定的,调度算法并不会考虑某个线程可能只是为了一个连接服务的,时间片到了就执行一下,哪怕这个线程一执行就会不得不继续睡眠.这样来回的唤醒.睡眠线程在次数不多的情况下,

半同步/半异步并发模式进程池实现

半同步/半异步并发模式:父进程监听到新的客户端连接请求后,以通信管道通知进程池中的某一子进程:"嘿,有新的客户连接来了,你去accept,然后处理下!",从而避免在进程间传递文件描述符.这种模式中,一个客户连接上的所有任务始终有同一个进程来处理. 具体细节,尽在代码中: #ifndef PROCESSPOOL_H #define PROCESSPOOL_H #include <sys/types.h> #include <sys/socket.h> #inclu

Spring 实现两种设计模式:工厂模式和单态模式

在Spring 中大量使用的以下两种设计模式:工厂模式和单态模式. 工厂模式可将Java 对象的调用者从被调用者的实现逻辑中分离出来,调用者只需关心被调用者必须满足的规则(接口) ,而不必关心实例的具体实现过程.这是面向接口编程的优势,能提高程序的解耦,避免所有的类以硬编码方式耦合在一起. 如果所有的类直接耦合,极易形成"骨牌效应",假如B 类调用了A 类,一旦A 类需要修改,则B 类也需要修改:假如C 类调用了B 类,则C 类也需要修改......依次类推,从而导致整个系统都需要改写

使用C++11 开发一个半同步半异步线程池

摘自:<深入应用C++11>第九章 实际中,主要有两种方法处理大量的并发任务,一种是一个请求由系统产生一个相应的处理请求的线程(一对一) 另外一种是系统预先生成一些用于处理请求的进程,当请求的任务来临时,先放入同步队列中,分配一个处理请求的进程去处理任务, 线程处理完任务后还可以重用,不会销毁,而是等待下次任务的到来.(一对多的线程池技术) 线程池技术,能避免大量线程的创建和销毁动作,节省资源,对于多核处理器,由于线程被分派配到多个cpu,会提高并行处理的效率. 线程池技术分为半同步半异步线程

半同步半异步高性能网络编程

网络编程的模式分为3种: 1. nginx的全异步方式,使用epoll处理网络数据.对于请求的处理也全然是异步的. 不论什么一个请求的处理假设花费了较长时间,那么nginx进程就会被处理操作堵塞,导致无法处理IO事件 2. 简单的一个连接一个线程方案.这样的方案无法处理大量并发的连接.适用mysql这类连接数不多的场景.当中也有一些优化的做法,比如使用线程池避免不断的创建销毁线程. 3. 半同步半异步方式,启动一个IO线程使用epoll处理网络数据. 当收到一个完整的请求包,把请求放到任务队列.

MySQL主从复制:半同步、异步

MySQL主从复制:半同步.异步 大纲 前言 如何对MySQL进行扩展? MySQL Replication WorkFlow MySQL主从复制模式 实战演练 MySQL异步复制实现 MySQL半同步复制实现 实验中的思考 总结 前言 本篇我们介绍MySQL Replication的相关内容, 我们首先介绍MySQL CLuster的实现原理和如何一步步构建一个MySQL Replication Cluster 看懂本文需要了解: MySQL基本操作,MySQL日志类型及其作用 如何对MySQ

半同步半异步线程池的实现(C++11)

简介 处理大量并发任务时,一个请求对应一个线程来处理任务,线程的创建和销毁将消耗过多的系统资源,并增加上下文切换代价.线程池技术通过在系统中预先创建一定数量的线程(通常和cpu核数相同),当任务到达时,从线程池中分配一个线程进行处理,线程在处理完任务之后不用销毁,等待重用. 线程池包括半同步半异步和领导者追随者两种实现方式.线程池包括三部分,第一层是同步服务层,它处理来自上层的任务请求.第二层是同步队列层,同步服务层中的任务将添加到队列中.第三层是异步服务层,多个线程同时处理队列中的任务. 先贴

(原创)C++半同步半异步线程池2

(原创)C++半同步半异步线程池 c++11 boost技术交流群:296561497,欢迎大家来交流技术. 线程池可以高效的处理任务,线程池中开启多个线程,等待同步队列中的任务到来,任务到来多个线程会抢着执行任务,当到来的任务太多,达到上限时需要等待片刻,任务上限保证内存不会溢出.线程池的效率和cpu核数相关,多核的话效率更高,线程数一般取cpu数量+2比较合适,否则线程过多,线程切换频繁反而会导致效率降低. 线程池有两个活动过程:1.外面不停的往线程池添加任务:2.线程池内部不停的取任务执行

c++11 实现半同步半异步线程池

感受: 随着深入学习,现代c++给我带来越来越多的惊喜- c++真的变强大了. 半同步半异步线程池: 其实很好理解,分为三层 同步层:通过IO复用或者其他多线程多进程等不断的将待处理事件添加到队列中,这个过程是同步进行的. 队列层:所有待处理事件都会放到这里.上一层事件放到这里,下一层从这里获取事件 异步层:事先创建好线程,让瞎猜呢和嗯不断的去处理队列层的任务,上层不关心这些,它只负责把任务放到队列里,所以对上层来说这里是异步的. 看张图: 如果你不熟悉c++11的内容 以下文章仅供参考 c++