两种高效的并发模式
并发模式是指I/O处理单元和多个逻辑单元之间协调完成任务的方法。服务器主要有两种并发编程模式:半同步/半异步模式和领导者/追随者模式。
半同步/半异步模式
在并发模式中,“同步”指的是程序完全按照代码序列的顺序执行;“异步”指的是程序的执行需要由系统事件来驱动。常见的系统事件包括中断,信号等。
按照同步方式运行的线程称为同步线程,按照异步方式运行的线程称为异步线程。显然,异步线程的执行效率高,实时性强。但编写异步方式执行的程序相对复杂,难以调试和扩展。而同步线程则相反,虽然效率相对较低,但是逻辑简单。
半同步/半异步模式中,同步线程用于处理客户逻辑;异步线程用于处理I/O事件。异步线程监听到客户请求后,将其封装成请求对象并插入请求队列中。请求队列将通知某个工作在同步模式的工作线程来读取并处理该请求对象。具体选择哪个工作线程来为新的客户请求服务,则取决于请求队列的设计。比如最简单的轮流选取工作线程的Round Robin算法,也可以通过条件变量或信号量来随机选择一个工作线程。
在服务器程序中,如果结合考虑两种事件处理模式和几种I/O模型,则半同步/半异步模式就存在多种变体。其中有一种变体称为半同步/半反应堆模式。
在图8-10中,异步线程只有一个,由主线程来充当。它负责监听所有socket
上的事件。如果监听socket
上有可读事件发生,即有新的连接请求到来,主线程就接收以得到新的连接socket
,然后往epoll
内核事件表中注册该socket
上的读写事件。如果连接socket
上有读写事件发生,即有新的客户请求到来或有数据要发送至客户端,主线程就将该连接socket
插入请求队列中。所有工作线程都睡眠在请求队列上,当有任务到来时,它们将通过竞争(比如申请互斥锁)获得任务的接管权。
在图8-10中,主线程插入请求队列中的任务是就绪的连接socket
。这说明该图所示的半同步/半反应堆模式采用的事件处理模式是Reactor
模式:它要求工作线程自己从socket
上读取客户请求和往socket
写入服务器应答。实际上,半同步/半反应堆模式也可以使用模拟的Proactor
事件处理模式,即由主线程来完成数据的读写。在这种情况下,主线程一般会将应用程序数据,任务类型等信息封装为一个任务对象,然后将其(或者指向该任务对象的一个指针)插入请求队列。工作线程从请求队列中取得任务对象之后,再进行处理,而无需进行读写操作。
半同步/半反应堆模式存在如下缺点。
- 主线程和工作线程共享请求队列。主线程往请求队列中添加任务,或者工作线程从请求队列取出任务,都需要对请求队列加所保护,从而白白浪费CPU时间。
- 每个工作线程在同一时间只能处理一个客户请求。如果客户数量较多,而工作线程较少,则请求队列中将堆积很多任务对象,客户端的响应速度将越来越慢。如果通过增加工作线程来解决这一问题,则工作线程的切换也耗费大量CPU时间。
图8-11描述了一种相对高效的半同步/半异步模式,它的每个工作线程都能同时处理多个客户连接。
图8-11中,主线程只管理监听socket
,连接socket
由工作线程来管理。当有新的连接到来时,主线程就接受之并将新返回的连接socket
派发给某个工作线程,此后该socket
上的任何I/O操作都由被选中的工作线程来处理,直到客户关闭连接。主线程向工作线程派发socket
的最简单的方式,是往它和工作线程之间的管道里写数据。工作线程检测到管道上有数据可读时,就分析是否是一个新的客户连接请求到来。如果是,则把该新socket
上的读写时间注册到自己的epoll
内核事件表中。
领导者/追随者模式
领导者/追随者模式是多个工作线程轮流获得事件源集合,轮流监听,分发并处理事件的一种模式。在任意时间点,程序都仅有一个领导者线程,它负责监听I/O事件。而其他线程都是追随者,它们休眠在线程池中等待称为新的领导者。当前的领导者如果检测到I/O事件,首先要从线程池中推选出新的领导者线程,然后处理I/O事件。此时,新的领导者等待新的I/O事件,而原来的领导者则处理I/O事件,二者实现了并发。