【Linux网络编程】高并发服务器编程 -- Reactor模式与Proactor模式

本文深入探讨Reactor与Proactor模式在网络编程中的应用,对比传统阻塞I/O模型,阐述两种模式的优势与工作原理,包括线程模型、事件处理流程及操作系统层面的异步I/O支持。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【Linux网络编程】高并发服务器编程 -- Reactor模式与Proactor模式

【0】传统阻塞 I/O 服务模型

特点

1. 采用阻塞式 I/O 模型获取输入数据;

2. 每个连接都需要独立的线程完成数据输入,业务处理,数据返回的完整操作;

存在问题

1. 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大;

2. 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费;

【1】Reactor 模式

Reactor 模式具有如下的优点 :

  • 1. 响应快,不必为单个同步事件所阻塞,虽然 Reactor 本身依然是同步的;
  • 2. 编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;
  • 3. 可扩展性,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源;
  • 4. 可复用性,reactor 框架本身与具体事件处理逻辑无关,具有很高的复用性;

I/O 复用结合线程池

Reactor 模式中有 2 个关键组成 :

1. Reactor : Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应;

2. Handlers : 处理程序执行 I/O 事件要完成的实际事件,Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作;

Reactor 模式构成示意图

EventHandler 抽象类表示 IO 事件处理器,它拥有 IO 文件句柄 Handle(通过get_handle获取),以及对 Handle 的操作handle_event(读/写等)方法;

Concrete EventHandler 继承于 EventHandler 可以对事件处理器的行为进行定制处理;

Reactor 类用于管理 EventHandler(注册、删除等),并使用 handle_events 实现事件循环,不断调用同步事件多路分离器(一般是内核)的多路分离函数 select,只要某个文件句柄被激活(可读/写等),select 就返回(阻塞),handle_events 就会调用与文件句柄关联的事件处理器的 handle_event 进行相关操作;

Reactor 处理时序示意图

通过 Reactor 的方式,可以将用户线程轮询 IO 操作状态的工作统一交给 handle_events 事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而 Reactor 线程负责调用内核的 select 函数检查 socket 状态。当有 socket 被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行 handle_event 进行数据读取等处理的工作。由于 select 函数是阻塞的,因此多路 I/O 复用模型也被称为异步阻塞 I/O 模型。注意,这里的所说的阻塞是指 select 函数执行时线程被阻塞,而不是指 socket。一般在使用 I/O 多路复用模型时,socket 都是设置为 NONBLOCK 的,不过这并不会产生影响,因为用户发起 I/O 请求时,数据已经到达了,用户线程一定不会被阻塞。

Reactor 模式的工作流程

  1. 主线程往 epoll 内核事件表中注册socket上的读就绪事件;
  2. 主线程调用 epoll_wait 等待 socket 上有数据可读;
  3. 当 socket 上有数据可读时,epoll_wait 通知主线程,主线程则将 socket 可读事件放入请求队列;
  4. 睡眠在请求队列上的某个工作线程被唤醒,它从 socket 读取数据,并处理客户请求,然后往 epoll 内核事件表中注册该 socket 上的写就绪事件;
  5. 主线程调用 epoll_wait 等待 socket 可写;
  6. 当 socket 可写时,epoll_wait 通知主线程,主线程将 socket 可写事件放入请求队列;
  7. 睡眠在请求队列上的某个工作线程(工作线程从请求队列读取事件后,根据事件的类型来决定如何处理它,没有必要区分读工作线程和写工作线程)被唤醒,它往 socket 上写入服务器处理客户请求的结果;

根据 Reactor 的数量和处理资源池线程的数量不同,有 3 种典型的实现 :

1. 单 Reactor 单线程

2. 单 Reactor 多线程

3. 主从 Reactor 多线程

主从 Reactor 多线程的变异模式

【2】Proactor 模式

Proactor 模型

Proactor 的特点是使用异步 I/O,所有的 I/O 操作都交由系统提供的异步 I/O 接口去执行;工作线程仅仅负责业务逻辑;
在 Proactor 中,用户函数启动一个异步的文件操作,同时将这个操作注册到多路复用器上,多路复用器并不关心文件是否可读或可写而是关心这个异步读操作是否完成;异步操作由操作系统完成,用户程序不需要关心;多路复用器等待直到有完成通知到来;当操作系统完成了读操作——将读到的数据复制到了用户先前提供的缓冲区之后,通知多路复用器相关操作已完成;多路复用器再调用相应的处理程序,处理数据;

详细方案

1. Proactor Initiator 创建 Proactor 和 Handler 对象,并将 Proactor 和 Handler 都通过 AsyOptProcessor (Asynchronous Operation Processor) 注册到内核;

2. AsyOptProcessor 处理注册请求,并处理 I/O 操作;

3. AsyOptProcessor 完成 I/O 操作后通知 Proactor;

4. Proactor 根据不同的事件类型回调不同的 Handler 进行业务处理;

5. Handler 完成业务处理;

Proactor 模式构成示意图

Proactor 模式中,用户线程将 AsynchronousOperation(读/写等)、Proactor 以及操作完成时的 CompletionHandler 注册到 AsynchronousOperationProcessor;
AsynchronousOperationProcessor 使用 Facade 模式提供了一组异步操作 API(读/写等)供用户使用,当用户线程调用异步 API后,便继续执行自己的任务,AsynchronousOperationProcessor 会开启独立的内核线程执行异步操作,从而实现真正的异步;
当异步IO操作完成时,AsynchronousOperationProcessor 将用户线程与 AsynchronousOperation 一起注册的 Proactor 和 CompletionHandler 取出,然后将 CompletionHandler 与 IO 操作的结果数据一起转发给 Proactor,Proactor 负责回调每一个异步操作的事件完成处理函数 handle_event;虽然 Proactor 模式中每个异步操作都可以绑定一个 Proactor 对象,但是一般在操作系统中,Proactor 被实现为 Singleton 模式,以便于集中分发操作完成事件;

Proactor 处理时序示意图

异步 I/O 模型中,用户线程直接使用内核提供的异步 I/O  API 发起 read 请求,且发起后立即返回,继续执行用户线程代码;此时用户线程已经将调用的 AsynchronousOperation 和 CompletionHandler 注册到内核;
操作系统开启独立的内核线程去处理 I/O 操作,当 read 请求的数据到达时,由内核负责读取 socket 中的数据,并写入用户指定的缓冲区中;
内核将 read 的数据和用户线程注册的 CompletionHandler 分发给内部 Proactor,Proactor 将 I/O 完成的信息通知给用户线程(一般通过调用用户线程注册的完成事件处理函数),完成异步 I/O;

Procator 模式的工作流程

  1. 主线程调用 aio_read 向内核注册 socket 上的读完成事件,并告诉内核用户缓冲区的位置,以及读操作完成时如何通知应用程序(可以用信号);
  2. 主线程继续处理其他逻辑;
  3. 当 socket 上的读数据被读入用户缓冲区后,内核向应用进程发送一个信号,已通知应用程序数据已经可用;
  4. 应用进程预先定义好的信号处理函数选择一个工作线程来处理处理客户请求,工作线程处理完客户请求之后,调用 aio_write 向内核注册 socket 的完成写事件,并告诉内核用户写缓冲区的位置,以及操作完成时如何通知应用程序(可以用信号);
  5. 主线程继续处理其他逻辑;
  6. 当用户缓冲区的数据被写入 socket 之后,内核将向应用程序发送一个信号,已通知应用程序数据已经发送完毕;
  7. 应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭 socket;

模拟 Proactor 模式

目前操作系统对异步IO的支持并非特别完善,更多的是采用IO多路复用模型模拟异步IO的方式;

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

两个模式的相同点

  • 对某个 IO 事件的事件通知 (即告诉某个模块,该 IO 操作可以进行或已经完成);在结构上的相同点,demultiplexor 负责提交 IO 操作(异步)、查询设备是否可操作(同步),然后当条件满足时,就回调注册处理函数;

两个模式的不同点

  • 异步情况下(Proactor),当回调注册的处理函数时,表示 IO 操作已经完成;
  • 同步情况下(Reactor),回调注册的处理函数时,表示 IO 设备可以进行某个操作(can read or can write),注册的处理函数此时开始提交操作;

参考
本博客为博主的学习实践总结,并参考了众多博主的博文,在此表示感谢,博主若有不足之处,请批评指正。

【1】UNIX网络编程

【2】Linux 高性能服务器编程

【3】Proactor模式&Reactor模式详解

【4】高性能网络编程(六):一文读懂高性能网络编程中的线程模型

【5】慕课专栏,网络编程之Netty一站式精讲

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值