高并发服务器设计精要:Reactor vs Proactor模式深入解析
本文深入探讨了高并发服务器设计中的两种核心模型:Reactor和Proactor。文章详细比较了它们的工作机制、优缺点及适用场景,并分析了它们在不同操作系统下的实现差异。对于需要构建高性能网络服务器的程序员来说,理解这两种模型对于优化I/O操作和提升系统性能至关重要。本文提供了清晰的技术对比,帮助程序员选择合适的模型以应对不同的开发挑战。
核心内容提要:Reactor与Proactor性能对比、高并发处理技术、I/O多路复用技术、异步I/O操作实例、同步I/O模拟Proactor、高性能网络服务器构建、操作系统I/O模型差异、事件驱动编程模型、多核CPU资源利用、服务器编程最佳实践
一、高并发编程概述
面对高并发场景下 I/O 操作的挑战,采用同步和异步的方式来处理“等待消息准备好”和“消息处理”两个阶段。本文详细比较 Reactor 和 Proactor 的工作机制、优缺点以及适用场景,分析其在不同操作系统下的实现差异。
对高并发编程,网络连接上的消息处理,可以分为两个阶段:等待消息准备好、消息处理。当使用默认的阻塞套接字时(例如 1 个线程捆绑处理 1 个连接),往往是把这两个阶段合而为一,这样操作套接字的代码所在的线程就得睡眠来等待消息准备好,这导致了高并发下线程会频繁的睡眠、唤醒,从而影响了CPU的使用效率。
高并发编程方法当然就是把两个阶段分开处理。即,等待消息准备好的代码段,与处理消息的代码段是分离的。当然,这也要求套接字必须是非阻塞的,否则,处理消息的代码段很容易导致条件不满足时,所在线程又进入了睡眠等待阶段。那么问题来了,等待消息准备好这个阶段怎么实现?它毕竟还是等待,这意味着线程还是要睡眠的!解决办法就是,线程主动查询,或者让1个线程为所有连接而等待!这就是IO多路复用了。多路复用就是处理等待消息准备好这件事的,但它可以同时处理多个连接!它也可能“等待”,所以它也会导致线程睡眠,然而这不要紧,因为它一对多、它可以监控所有连接。这样,当我们的线程被唤醒执行时,就一定是有一些连接准备好被我们的代码执行了。
作为一个高性能服务器程序通常需要考虑处理三类事件: I/O事件,定时事件及信号。两种高效的事件处理模型:Reactor和Proactor。
二、Reactor模型
首先来回想一下普通函数调用的机制:程序调用某函数,函数执行,程序等待,函数将结果和控制权返回给程序,程序继续处理。Reactor释义“反应堆”,是一种事件驱动机制。和普通函数调用的不同之处在于:应用程序不是主动的调用某个API完成处理,而是恰恰相反,Reactor逆置了事件处理流程,应用程序需要提供相应的接口并注册到Reactor上,如果相应的时间发生,Reactor将主动调用应用程序注册的接口,这些接口又称为“回调函数”。
Reactor模式是处理并发I/O比较常见的一种模式,用于同步I/O,中心思想是 将所有要处理的I/O事件注册到一个中心I/O多路复用器上,同时主线程/进程阻塞在多路复用器上;一旦有I/O事件到来或是准备就绪(文件描述符或socket可读、写),多路复用器返回并将事先注册的相应I/O事件分发到对应的处理器中。
Reactor模型有三个重要的组件:
- 多路复用器:由操作系统提供,在linux上一般是select, poll, epoll等系统调用。
- 事件分发器:将多路复用器中返回的就绪事件分到对应的处理函数中。
- 事件处理器:负责处理特定事件的处理函数。
具体流程如下:
- 注册读就绪事件和相应的事件处理器;
- 事件分离器等待事件;
- 事件到来,激活分离器,分离器调用事件对应的处理器;
- 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。
Reactor模式是构建高性能网络服务器的必备技术之一,主要优点:
- 快速响应:该模式避免了因单个同步操作而造成的阻塞,尽管Reactor本身仍为同步机制。
- 编程简洁:Reactor模式能够大幅减少复杂多线程和同步问题的出现,避免了多线程/进程间切换的性能开销。
- 良好扩展性:通过增加Reactor实例的数量,可以更加有效地利用CPU资源。
- 高复用性:Reactor框架独立于具体事件处理逻辑,具有很高的复用潜力。
Reactor模型开发效率上比起直接使用IO复用要高,它通常是单线程的,设计目标是希望单线程使用一颗CPU的全部资源,但也有附带优点,即每个事件处理中很多时候可以不考虑共享资源的互斥访问。可是缺点也是明显的,现在的硬件发展,已经不再遵循摩尔定律,CPU的频率受制于材料的限制不再有大的提升,而改为是从核数的增加上提升能力,当程序需要使用多核资源时,Reactor模型就会悲剧,为什么呢?
如果程序业务很简单,例如只是简单的访问一些提供了并发访问的服务,就可以直接开启多个反应堆,每个反应堆对应一颗CPU核心,这些反应堆上跑的请求互不相关,这是完全可以利用多核的。例如Nginx这样的http静态服务器。
三、Proactor模型
与Reactor模式相对,Proactor模型通过异步操作来处理I/O事件,是一种异步事件驱动的网络编程模式。
工作流程:在Proactor模型中,应用程序发起异步I/O操作并立即返回,继续执行其他任务。I/O操作完成后,操作系统或异步I/O库会通过回调机制通知应用程序,然后应用程序处理结果。
特点:
- 异步处理:Proactor允许应用程序在等待I/O操作完成的同时执行其他操作,提高了资源利用率。
- 事件驱动:类似Reactor,Proactor也使用事件驱动的方式,但其处理的“完成事件”是由操作系统或I/O库指示。
优点:
- 可以同时处理多个I/O请求,适合高并发场景。
- 由于使用异步I/O,Proactor模式减少了线程间的上下文切换开销。
- 开发者可以专注于处理完成的I/O事件,而无需管理底层的多线程或同步机制。
适合网络通信、大规模数据处理等需要高效I/O操作的应用场景,例如Web服务器、数据库系统等。
在实现Proactor模型时,常用的技术包括操作系统提供的异步I/O接口(如Windows的IOCP)和高阶库(如Boost.Asio)。
具体流程如下:
- 处理器发起异步操作,并关注I/O完成事件。
- 事件分离器等待操作完成事件。
- 分离器等待过程中,内核并行执行实际的I/O操作,并将结果数据存入用户自定义缓冲区,最后通知事件分离器读操作完成。
- I/O完成后,通过事件分离器呼唤处理器。
- 事件处理器处理用户自定义缓冲区中的数据。
从上面的处理流程,我们可以发现 Proactor 模型最大的特点是使用异步I/O。所有的I/O操作都交由系统提供的异步I/O接口去执行。工作线程仅仅负责业务逻辑。在Proactor中,用户函数启动一个异步的文件操作。同时将这个操作注册到多路复用器上。多路复用器并不关心文件是否可读或可写而是关心这个异步读操作是否完成。异步操作是操作系统完成,用户程序不需要关心。多路复用器等待直到有完成通知到来。当操作系统完成了读文件操作——将读到的数据复制到了用户先前提供的缓冲区之后,通知多路复用器相关操作已完成。多路复用器再调用相应的处理程序,处理数据。
Proactor增加了编程的复杂度,但给工作线程带来了更高的效率。Proactor可以在系统态将读写优化,利用I/O并行能力,提供一个高性能单线程模型。在windows上,由于没有epoll这样的机制,因此提供了IOCP来支持高并发, 由于操作系统做了较好的优化,windows较常采用Proactor的模型利用完成端口来实现服务器。在linux上,在2.6内核出现了aio接口,但aio实际效果并不理想,它的出现,主要是解决poll性能不佳的问题,但实际上经过测试,epoll的性能高于poll+aio,并且aio不能处理accept,因此 Linux 主要还是以Reactor模型为主。
在不使用操作系统提供的异步I/O接口的情况下,还可以使用Reactor来模拟Proactor, 差别是:使用异步接口可以利用系统提供的读写并行能力,而在模拟的情况下,这需要在用 户态实现。具体的做法只需要这样:
- 注册读事件(同时再提供一段缓冲区) ;
- 事件分离器等待可读事件 ;
- 事件到来,激活分离器,分离器(立即读数据,写缓冲区)调用事件处理器 ;
- 事件处理器处理数据,删除事件(需要再用异步接口注册)。
我们知道,Boost.asio库采用的即为Proactor模型。不过Boost.asio库在Linux平台采用 epoll实现的Reactor来模拟Proactor,并且另外开了一个线程来完成读写调度。
四、同步 I/O 模拟Proactor模型
- 主线程往epoll内核事件表中注册socket上的读就绪事件。
- 主线程调用epoll_wait等待socket上有数据可读。
- 当socket上有数据可读时,epoll_wait通知主线程。主线程从socket循环读取数据,直到没有更多数据可读,然后将读取到的数据封装成一个请求对象并插入请求队列。
- 睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往epoll内核事件表中注册socket上的写就绪事件。
- 主线程调用epoll_wait等待socket可写。
- 当socket可写时,epoll_wait通知主线程。主线程往socket上写入服务器处理客户请求的结果。
两个模式的相同点,都是对某个IO事件的事件通知(即告诉某个模块,这个IO操作可以进行或已经完成)。在结构上两者也有相同点:demultiplexor负责提交IO操作(异步)、查询设备是否可操作(同步),然后当条件满足时,就回调注册处理函数。
不同点在于,异步情况下(Proactor),当回调注册的处理函数时,表示IO操作已经完成;同步情况下(Reactor),回调注册的处理函数时,表示 IO 设备可以进行某个操作(can read or can write),注册的处理函数这个时候开始提交操作。
五、总结
在处理高并发网络编程时,Reactor和Proactor模型各自提供了不同的优势和应用场景。Reactor适用于需要高效处理大量并发连接的场景,能够简化多线程管理并减少上下文切换的开销。而Proactor则通过完全异步的方式提高了并发处理的能力,特别是在I/O密集型的应用中展现出更高的性能。