Server actor
同步IO模型通常用于实现Reactor模式, 异步IO模型则用于实现Proactor模式。
Reactor
Reactor模式要求主线程(I/O处理单元,下同)只负责监听文件描述上是否有事件发生,有的话就立即将该事件通知工作线程(逻辑单元,下同)。除此之外,主线程不做任何其他实质性的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。
Reactor工作流程∶
- 主线程往 epoll内核事件表中注册 socket 上的读就绪事件。
- 主线程调用epoll_wait 等待socket上有数据可读。
- 当socket上有数据可读时,epoll_wait 通知主线程。主线程则将socket可读事件放入请求队列。
- 睡眠在请求队列上的某个工作线程被唤醒,它从socket读取数据,并处理客户请求,然后往 epoll 内核事件表中注册该 socket 上的写就绪事件。
- 主线程调用epoll_wait 等待socket可写。
- 当socket可写时,epoll_wait 通知主线程。主线程将socket可写事件放入请求队列。
- 睡眠在请求队列上的某个工作线程被唤醒,它往socket上写入服务器处理客户请求的结果。
Procactor
将多有I/O操作都交给主线程和内核来处理, 工作线程仅负责业务逻辑
Proactor工作流程∶
- 主线程调用aio_read 函数向内核注册 socket 上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序(这里以信号为例,详情请参考sigevent的 man 手册)。
- 主线程继续处理其他逻辑。
- 当socket上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据已经可用。
- 应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求之后,调用 aio_write 函数向内核注册 socket 上的写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成时如何通知应用程序(仍然以信号为例)。
- 主线程继续处理其他逻辑。
- 当用户缓冲区的数据被写人 socket 之后,内核将向应用程序发送一个信号,以通知应用程序数据已经发送完毕。
- 应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭 socket。
模拟Proactor
使用同步I/O模型(仍然以epoll_wait为例)模拟出的Proactor模式的工作流程∶
- 主线程往 epoll 内核事件表中注册 socket 上的读就绪事件。
- 主线程调用 epoll_wait 等待 socket上有数据可读。
- 当 socket 上有数据可读时,epoll_wait通知主线程。主线程从socket循环读取数据,直到没有更多数据可读,然后将读取到的数据封装成一个请求对象并插入请求队列。
- 睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往 epoll 内核事件表中注册 socket上的写就绪事件。
- 主线程调用 epoll_wait 等待 socket 可写。
- 当 socket 可写时,epoll wait 通知主线程。主线程往 socket 上写人服务器处理客户请求的结果。
半同步/半异步
在I/O模型中,“同步"和"异步"区分的是内核向应用程序通知的是何种 I/O 事件(是就绪事件还是完成事件),以及该由谁来完成I/O读写(是应用程序还是内核)。在并发模式中,“同步"指的是程序完全按照代码序列的顺序执行∶“异步"指的是程序的执行需要由系统事件来驱动。常见的系统事件包括中断、信号等。
工作流程
异步线程用于处理 I/O事件,相当于图8-4中的I/O处理单元。异步线程监听到客户请求后,就将其封装成请求对象并插入请求队列中。请求队列将通知某个工作在同步模式的工作线程来读取并处理该请求对象。
反应堆模式
在服务器程序中,如果结合考虑两种事件处理模式和几种 I/O模型,则半同步/半异步模式就存在多种变体。其中有一种变体称为半同步/半反应堆(halfsynchalf-reactive)模式
异步线程只有一个,由主线程来充当。它负责监听所有socket上的事件。如果监听 socket上有可读事件发生,即有新的连接请求到来,主线程就接受之以得到新的连接 socket,然后往epoll内核事件表中注册该socket上的读写事件。如果连接socket上有读写事件发生,即有新的客户请求到来或有数据要发送至客户端,主线程就将该连接socket插入请求队列中。所有工作线程都睡眠在请求队列上,当有任务到来时,它们将通过竞争(比如申请互斥锁)获得任务的接管权。这种竞争机制使得只有空闲的工作线程才有机会来处理新任务,这是很合理的。
半同步 / 半反应堆模式存在如下缺点∶
- 主线程和工作线程共享请求队列。主线程往请求队列中添加任务,或者工作线程从详 求队列中取出任务,都需要对请求队列加锁保护,从而白白耗费 CPU时间。
- 每个工作线程在同一时间只能处理一个客户请求。如果客户数量较多,而工作线程较 少,则请求队列中将堆积很多任务对象,客户端的响应速度将越来越慢。如果通过增加工作线程来解决这一问题,则工作线程的切换也将耗费大量 CPU 时间。
高效半同步半异步
主线程只管理监听 socket,连接 socket 由工作线程来管理。当有新的连接到来时,主线程就接受之并将新返回的连接 socket 派发给某个工作线程,此后该新 socket上的任何I/O 操作都由被选中的工作线程来处理,直到客户关闭连接。主线程向工作线程派发 socket 的最简单的方式,是往它和工作线程之间的管道里写数据。工作线程检测到管道上有数据可读时,就分析是否是一个新的客户连接请求到来。如果是,则把该新 socket上的读写事件注册到自己的 epoll 内核事件表中。
领导者/追随者模式
领导者/追随者模式是多个工作线程轮流获得事件源集合,轮流监听、分发并处理事件的一种模式。在任意时间点,程序都仅有一个领导者线程,它负责监听 I/O事件。而其他线程则都是追随者,它们休眠在线程池中等待成为新的领导者。当前的领导者如果检测到I/O事件,首先要从线程池中推选出新的领导者线程,然后处理L/O事件。此时,新的领导者等待新的 I/O 事件,而原来的领导者则处理 I/O 事件,二者实现了并发。
领导者/追随者模式包含如下几个组件∶句柄集(HandleSet)、线程集(ThreadSet)、事件处理器(EventHandler)和具体的事件处理器(ConcreteEventHandler)。
1.句柄集
- 句柄(Handle)用于表示 I/O 资源,在Linux下通常就是一个文件描述符。句柄集管理众多句柄,它使用wait_for event方法来监听这些句柄上的I/O事件,并将其中的就绪事件通知给领导者线程。领导者则调用绑定到Handle上的事件处理器来处理事件。领导者将 Handle 和事件处理器绑定是通过调用句柄集中的 register_handle方法实现的。
2.线程集
- 这个组件是所有工作线程(包括领导者线程和追随者线程)的管理者。它负责各线程之间的同步,以及新领导者线程的推选。线程集中的线程在任一时间必处于如下三种状态之一∶
- Leader∶ 线程当前处于领导者身份,负责等待句柄集上的 I/O 事件。
- Processing∶线程正在处理事件。领导者检测到I/O事件之后,可以转移到Processing状态来处理该事件,并调用 promote_new_leader方法推选新的领导者;也可以指定其他追随者来处理事件(Event Handoff),此时领导者的地位不变。当处于Processing状态的线程处理完事件之后,如果当前线程集中没有领导者,则它将成为新的领导 者,否则它就直接转变为追随者。
- Follower∶线程当前处于追随者身份,通过调用线程集的join方法等待成为新的领导者,也可能被当前的领导者指定来处理新的任务。图 8-13 显示了这三种状态之间的转换关系。
需要注意的是,领导者线程推选新的领导者和追随者等待成为新领导者这两个操作都将修改线程集,因此线程集提供一个成员 Synchronizer来同步这两个操作,以避免竞态条件。
3. 事件处理器和具体的事件处理器
事件处理器通常包含一个或多个回调函数 handle_event。这些回调函数用于处理事件对应的业务逻辑。事件处理器在使用前需要被绑定到某个句柄上,当该句柄上有事件发生时,领导者就执行与之绑定的事件处理器中的回调函数。具体的事件处理器是事件处理器的派生类。它们必须重新实现基类的 handle_event 方法,以处理特定的任务。
由于领导者线程自己监听 I/O 事件并处理客户请求,因而领导者/追随者模式不需要在线程之间传递任何额外的数据,也无须像半同步/半反应堆模式那样在线程之间同步对请求队列的访问。但领导者/追随者的一个明显缺点是仅支持一个事件源集合,因此也无法像图 8-11 所示的那样,让每个工作线程独立地管理多个客户连接。