并发模式是指IO处理单元和多个逻辑单元之间协调完成任务的方法。服务器主要有两种并发编程模式:半同步/半异步 、领导者/追随者 模式
半同步/半异步
在IO模型中,同步和异步指的是是内核向应用程序是何种IO事件(是就绪事件还是完成事件),以及该有谁来完成IO读写(是应用程序还是内核)
在并发模式中,“同步”指的是程序完全按照代码序列的顺序执行;异步指的是程序的执行需要由系统事件(比如中断、信号等)的驱动
异步线程执行效率高,实时性强,但是难调试和难扩展;同步线程效率相对低,实时性差,但是逻辑简单容易编写。因此,对于像服务器这种即要求较好的实时性,有要求能够同时处理多个客户请求的应用程序,我们应该同时使用同步线程和异步线程实现,即采用半同步/半异步模式来实现
半同步/半异步模式中,同步线程用于处理客户逻辑;异步线程用于处理IO事件。 异步线程监听到客户请求后,就将其封装成请求对象并插入请求队列中。请求队列将通知某个工作在同步模式的工作线程读取并处理该请求对象。具体选择哪个工具来请求新的客户请求服务,则取决于请求队列的设计:
- 轮流选取工作线程的Round Robin算法
- 通过条件变量、信号量随机选择工作线程
在服务器程序中,如果结合考虑两种事件处理模式和几种IO模型,则半同步/半异步模式就存在很多变体。如下
半同步/半反应堆(half-sync/half-reactive
)模式
-
异步线程只有一个,由主线程充当,负责监听所有socket上的事件:
- 如果监听socket上有可读事件发生,即有新的连接请求到来,主线程就接受之,以得到新的连接socket,然后往epoll内核事件表中注册该socket上的读写事件。
- 如果监听socket上有读写事件发生,即有新的客户请求或者有数据要发送到客户端,主线程就将该连接socket插入请求队列中
-
所有工作线程都睡眠在请求队列上,当有任务到来时,他们将通过竞争(比如申请互斥锁)获得任务的接管权。这种竞争机制使得只有空闲的工作线程才有机会来处理新任务
上图中,主线程插入请求队列中的任务是就绪的连接socket,这说明上图采用的事件处理模式是Reactor模式:它要求工作线程自己从socket上读取客户请求和往socket写入服务器应答。这就是该模式名称中的(half-reactive
)的含义。
当然,半同步/半反应堆模式也可以使用模拟的Proactor事件处理模式,即由主线程来完成数据的读写。这时,主线程一般会将应用程序数据,任务类型等信息封装为一个任务对象,然后将其(或者指向该任务对象的一个指针)插入请求队列。工作线程从请求队列中取得任务对象知乎,就可以直接处理不需要进行读写操作了。
半同步/半反应堆模式的缺点:
- 主线程和工作线程共享请求队列。主线程往请求队列中加任务,或者工作线程从请求队列中取出任务,都需要对请求队列加锁保护,从而白白耗费CPU时间
- 每个工作线程在同一时间只能处理一个客户请求。如果客户数量较多,而工作线程较少,则请求队列中将堆积很多任务对象,客户端的响应速度将越来越慢。如果通过增加工作线程来解决这个问题,则工作线程的切换也将耗费大量的时间
高效的半同步/半异步模式
每个工作线程都能同时处理多个客户连接
- 主线程只管理监听socket,连接socket由工作线程管理。
- 当由新连接到来时,主线程就接受,并将新返回的连接socket派发给某个工作线程
- 主线程向工作线程派发socket的最简单的方式,是往它和工作线程之间的管理里写数据。
- 主线程将连接socket派发完毕之后,该新socket上的任何IO操作都有被选中的工作线程来处理,直到客户关闭连接
- 工作线程检测到管道上有数据可读时,就分析是否是一个新的客户连接请求到来。如果是,则把该新socket上的读写事件注册到自己的epoll内核事件表中
- 当由新连接到来时,主线程就接受,并将新返回的连接socket派发给某个工作线程
每个线程(主线程和工作线程)都维持自己的事件循环,他们各自独立的监听不同的事件。每个线程都工作在异步模式,所以它并不是严格意义上的半同步/半异步模式
领导者/追随者模式
领导者/追随者模式是多个工作线程轮流获得事件源集合,轮流监听、分发并处理事件的一种模式。在任意时间点,程序都仅有一个领导者线程,它负责监听IO事件,而其他线程则都是追随者,它们休眠在线程池中等待成为新的领导者。当前的领导者如果监测到IO事件,则先从线程池中推选出新的领导者线程,然后处理IO事件。此时,新的领导者等待新的IO事件,而原来的领导者处理IO事件,实现了并发。
领导者/追随者模式包含以下几个组件:句柄集(HandlerSet)、线程集(ThreadSet)、事件处理器(EventHandler)、和具体的事件处理器(ConcreteEventHandler)
句柄集(HandlerSet):
- 句柄(handle)用于表示IO资源,在linux下通常是一个文件描述符
- 句柄集管理众多句柄,它使用
wait_for_event
方法来监听这个句柄上的IO事件,并将其中的就绪事件通知给领导者线程。领导者线程则调用绑定在Handler
上的事件处理器
来处理事件。领导者将Handler
和事件处理器绑定是通过调用句柄集中的register_handler
实现的
线程集(ThreadSet):
-
这个组件是所有工作线程(包括领导者线程和追随者线程)的管理者。它负责各线程之间的同步,以及新领导者的推选。线程集中的线程在任一时间一定处于下面三种状态之一:
-
Leader
:线程当前处于领导者身份,负责等待句柄集上的IO事件 -
Processing
:线程正在处理事件。领导者检测到IO事件之后,可以转移到Processing
状态来处理该事件,并调用promote_new_leader
方法推选新的领导者。也可以指定其他追随者(Event handoff)来处理事件,此时领导者的地位不变。当处于Processing
状态的线程处理完事件之后,如果当前线程集中没有领导者,则它将成为新的领导者,否则它就直接变成追随者 -
Follower
:线程当前处于追随者身份,通过调用线程集的join方法等待成为新的领导者,有可能被当前的领导着指定来处理新的任务
-
-
注意:领导者线程推选新的领导者和追随者等待成为新的领导者这两个操作都将修改线程集,因此线程集提供一个成员
Synchronizer
来同步这两个操作,以避免竞态条件
事件处理器和具体的事件处理器
- 事件处理器通常包含一个或者多个回调函数
handle_event
。这些回调函数用于处理事件对应的业务逻辑。事件处理器在使用前需要被绑定到某个句柄上,当该句柄上有事件发生时,领导者就执行与之绑定的事件处理器中额回调方法。具体的事件处理器是事件处理器的派生类。它们必须重新实现类的handle_event
方法,以处理特定的任务
由于领导者线程自己监听IO事件并处理客户请求,因而领导者/追随者模式不需要在线程之间传递任务额外数据,也不需要向半同步/半反应堆那样在线程之间同步对请求队列的访问,但是领导者/追随者的缺点是仅支持一个事件源集合,无法让每个工作线程独立的管理多个客户连接