Linux编程入门三网络编程六 事件处理模式和并发模式_事件处理

Reactor模式与Proactor模式

随着网络设计模式的兴起,Reactor和Proactor事件处理模式应运而生。同步I/O模型通常用于实现Reactor模式,异步I/O模型则用于实现Proactor模式。

基于Reactor模式的I/O框架库包含如下几个组件:句柄(Handle)、事件多路分发器(EventDemultiplexer)、事件处理器(EventHandler)、具体的事件处理器(ConcreteEventHandler)、Reactor。
句柄:I/O框架库要处理的对象,即I/O事件、信号和定时事件,统一称为事件源。一个事件源通常和一个句柄绑定在一起。句柄的作用是,当内核检测到就绪事件时,它将通过句柄来通知应用程序这一事件。在Linux环境下,I/O事件对应的句柄是文件描述符,信号事件对应的句柄就是信号值。
事件多路分发器:事件的到来是随机的、异步的。我们无法预知程序何时收到一个客户连接请求,又亦或收到一个暂停信号。所以程序需要循环地等待并处理事件,这就是事件循环。在事件循环中,等待事件一般使用I/O复用技术来实现。**I/O框架一般将系统支持的各种I/O复用系统调用封装成统一的接口,称为事件多路分发器。**事件多路分发器的demultiplex方法是等待事件的核心函数,其内部调用的是select、poll、epoll_wait等函数。此外,事件多路分发器还需要实现register_event和remove_event方法,以供调用者往事件多路分发器中添加事件和从事件多路分发器中删除事件。
事件处理器和具体事件处理器:事件处理器执行事件对应的业务逻辑。它通常包含一个或多个handle_event回调函数,这些回调函数在事件循环中被执行。I/O框架库提供的事件处理器通常是一个接口,用户需要继承它来实现自己的事件处理器,即具体事件处理器。因此,事件处理器中的回调函数一般被声明为虚函数,以支持用户的扩展。此外,事件处理器一般还提供一个get_handle方法,它返回与该事件处理器关联的句柄。那么,事件处理器和句柄有什么关系?当事件多路分发器检测到有事件发生时,它是通过句柄来通知应用程序的。因此,我们必须将事件处理器和句柄绑定,才能在事件发生时获取到正确的事件处理器。
Reactor:Reactor是I/O框架库的核心。它提供的几个主要方法是:

  1. handle_events该方法执行事件循环。它重复如下过程:等待事件,然后依次处理所有就绪事件对应的事件处理器。
  2. register_handler该方法调用事件多路分发器的register_event方法来往事件多路分发器中注册一个事件。
  3. remove_handler该方法调用事件多路分发器额remove_event方法来删除事件多路分发器中的一个事件。
  4. Linux编程入门三网络编程六 事件处理模式和并发模式_事件处理_02

  5. Reactor要求主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知工作线程(逻辑单元)。除此之外,主线程不做任何其他实质性的工作。读写数据,接收新的连接,以及处理客户请求均在工作线程中完成。使用同步I/O模型(以epoll_wait为例)实现Reactor模式的工作流程是:
  6. 主线程往epoll内核事件表中注册socket上的读就绪事件。
  7. 主线程调用epoll_wait等待socket上有数据可读。
  8. 当socket上有数据可读时,epoll_wait通知主线程。主线程则将socket可读事件放入请求队列。
  9. 睡眠在请求队列上的某个工作线程被唤醒,它从socket读取数据,并处理客户请求,然后往epoll内核事件表中注册该socket上的写就绪事件。
  10. 主线程调用epoll_wait等待socket可写。
  11. 当socket可写时,epoll_wait通知主线程。主线程将socket可写事件放入请求队列。
  12. 睡眠在请求队列上的某个工作线程被唤醒,它往socket上写入服务器处理客户请求的结果。
  13. Linux编程入门三网络编程六 事件处理模式和并发模式_句柄_03

  14. Proactor模式将所有I/O操作都交给主线程和内核来处理,工作线程仅仅负责业务逻辑。 使用异步I/O模型(以aio_read和aio_write为例)实现的Proactor模式的工作流程是:
  15. 主线程调用aio_read函数向内核注册socket上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序。
  16. 主线程继续处理其他逻辑
  17. 当socket上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据已经可用。
  18. 应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求之后,调用aio_write函数向内核注册socket上的写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成时如何通知应用程序(仍然以信号为例)。
  19. 主线程继续处理其他逻辑。
  20. 当用户缓冲区的数据被写入socket之后,内核将向应用程序发送一个信号,以通知应用程序数据已经发送完毕。
  21. 应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭socket。

Linux编程入门三网络编程六 事件处理模式和并发模式_句柄_04


连接socket上的读写事件是通过aio_read/aio_write向内核注册的,因此内核将通过信号来向应用程序报告连接socket上的读写事件。所以主线程中的epoll_wait调用仅能用来检测监听socket上的连接请求事件,而不能用来检测连接socket上的读写事件。

模拟Proactor模式
使用同步I/O方式模拟出Proactor模式的一种方法。原理是主线程执行数据读写操作,读写完成之后,主线程向工作线程通知这一“完成事件”。从工作线程的角度来看,它们就直接获得数据读写结果。使用同步I/O模型(epoll_wait为例)模拟出Proactor模式的工作流程:

  1. 主线程往epoll内核事件表中注册socket上的读就绪事件。
  2. 主线程调用epoll_wait等待socket上有数据可读。
  3. 当socket上有数据可读时,epoll_wait通知主线程。主线程从socket循环读取数据。直到没有更多数据可读,然后将读取到的数据封装成一个请求对象并插入请求队列。
  4. 睡眠在请求队列上的某个工作线程被唤醒,它获取请求对象并处理客户请求,然后往epoll内核事件表中注册socket上的写就绪事件。
  5. 主线程调用epoll_wait等待socket可写。
  6. 当socket可写时,epoll_wait通知主线程。主线程往socket上写入服务器处理客户端请求的结果。

Linux编程入门三网络编程六 事件处理模式和并发模式_事件处理_05

并发模式:半同步/半异步模式 领导者/追随者模式

并发模型是指I/O处理单元和多个逻辑单元之间协调完成任务的方法。

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

Linux编程入门三网络编程六 事件处理模式和并发模式_事件处理_06


按照同步方式运行的线程称为同步线程,按照异步方式运行的线程称为异步线程。对于像服务器这种既要求较好的实时性,又要求能同时处理多个客户请求的应用程序,应该同时使用同步线程和异步线程来实现,即采用半同步/半异步模式来实现。

半同步/半异步模式中,同步线程用于处理客户逻辑,异步线程用于处理I/O事件。异步线程监听到客户端请求后,将其封装成请求对象并插入请求队列中。请求队列将通知某个工作在同步模式的工作线程来读取并处理该请求对象。具体选择哪个工作线程来为新的客户请求服务,则取决于请求队列的设计。比如最简单的轮流选取工作线程的Round Robin算法,也可以通过条件变量或信号量来随机地选择一个工作线程。

Linux编程入门三网络编程六 事件处理模式和并发模式_主线程_07


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

句柄集(HandleSet)用于管理句柄(句柄用于表示I/O资源,在Linux下通常是一个文件描述符),它使用wait_for_event方法来监听这些句柄上的I/O事件,并将其中的就绪事件通知给领导线程。领导者则调用绑定到Handle上的事件处理器来处理事件。领导者将Handle和事件处理器绑定是通过调用句柄集中的register_handle方法实现的。线程集是所有工作线程的管理者。它负责各线程之间的同步,以及新领导者线程的推选。事件处理器通常包含一个或多个回调函数handle_event,这些回调函数用于处理事件对应的业务逻辑。事件处理器在使用前需要被绑定到某个句柄上,当该句柄上有事件发生时,领导者就执行与之绑定的事件处理器中的回调函数。

Linux编程入门三网络编程六 事件处理模式和并发模式_句柄_08