在PPC或者TPC中,每处理一个请求都要创建一个进程或者线程,连接关闭后进程或线程被销毁,进程或线程的创建和销毁过程本身占用了很多系统资源,在此基础上,基于池化思想,服务器接收到请求后,交由进程池中的一个空间进程处理(线程池就是空闲线程)。
一个连接与服务器交互,就是从连接中读取数据,服务器处理数据,将结果写回连接。当若干连接与服务器交互时,何种IO模式适合处理若干连接,下面将基于五种IO模型进行分析:
阻塞IO
若使用阻塞IO,服务器主进程(或主线程)将阻塞在等待写入的连接上,即使其他连接有数据写入,主进程(或主线程)也不能处理,而每一个连接使用一个进程(或者线程),则又会回到PPC(或TPC)模式。故阻塞IO模型不适合。
非阻塞IO
若使用非阻塞IO,服务器主进程(或主线程)将对等待写入的连接进行不断轮询,即使其他连接有数据写入,主进程(或主线程)也不能处理,而每一个连接使用一个进程(或者线程),则又会回到PPC(或TPC)模式。故非阻塞IO模型不适合。
IO多路复用
若使用IO多路复用,服务器主进程(或主线程)将对多个连接进行轮询,只要有一个连接有数据写入,就可以将连接数据交由进程池(或线程池)处理。该网络编程模式为Reactor。
信号驱动IO
信号驱动IO模型本身可以处理多个描述符,但若设计服务器,尤其是HTTP服务器,由于HTTP基于TCP,而TCP中会在多个阶段频繁触发SIGIO信号,导致信号处理函数难以区分具体的 I/O 事件。所以TCP服务器不适合使用信号驱动IO,信号驱动IO反而常用于UDP。
异步IO
若使用异步IO,服务器主进程(或主线程)会异步处理连接。该网络编程模式为Proactor。
经过以上分析,可以发现,对于要求可靠连接的服务器,其网络编程模式基于IO多路复用和异步IO更为适合,前者称之为Refactor模式,后者称之为Proactor模式。
Reactor
Reactor基于IO多路复用模型实现,有两个主要组成,一个是Reactor,用于分发dispatcher 请求,一个是资源池,资源池可以是单进程(或单线程),也可以是多进程(或多线程)。由此可以获得四种Reactor实现方案:
- 单Reactor,单进程(或单线程)
- 单Reactor,多进程(或多线程)
- 多Reactor,单进程(或单线程)
- 多Reactor,多进程(或多线程)
单Reactor,单进程(或单线程)
下面将以进程为例。
Reactor负责分发请求,新的连接交由accepter,其他交由handler处理,handler会从连接中读取数据、处理数据、写入数据。
这些流程都发生在单个进程中,当进程在执行handler时,则其他操作都会停止,该种方案无法利用到多核CPU,应用场景有限,只适用于业务处理及其快速的场景。Redis的核心部分采用该种方案。
在具体实现上,使用单进程和单线程往往取决于具体语言。C语言编写的程序一般使用单Reactor单进程,C语言程序启动后本身就是一个纯粹的进程。Java语言编写的一般使用单Reactor单线程,JVM本身是一个包含若干线程的进程,Java用户程序在JVM中以子线程形式运行。
单Reactor,多进程(或多线程)
下面将以线程为例。
Reactor负责分发请求,新的连接交由accepter,其他交由handler处理,handler会从连接中读取数据,处理数据不再由主线程处理,而是交由子线程处理,子线程处理完成后将数据返回给handler,handler写入数据到连接。
Handler 只负责响应事件,不进行业务处理;Handler 通过 read 读取到数据后,会发给 Processor 进行业务处理。Processor 会在独立的子线程中完成真正的业务处理,然后将响应结果发给主进程的 Handler 处理;Handler 收到响应后通过 send 将响应结果返回给 client。
单Reactor多线程存在的问题:
- 多线程数据共享和访问比较复杂。
- Reactor 承担所有事件的监听和响应,只在主线程中运行,瞬间高并发时会成为性能瓶颈。
单Reactor,多进程(或多线程)方案中通常不会使用多进程而是使用多线程,由于线程通信比进程通信更加容易,使得线程方案复杂度更低。
多Reactor,单进程(或单线程)
多Reactor,单进程(或单线程)方案并没有太大实际意义,实现上不会采用该方案。真正的工作负载由进程池或线程池承载,对于单进程或者单线程方案来说,瓶颈在于无法同时处理多个任务,多Reactor是不必要的。
多Reactor,多进程(或多线程)
以进程为例。
主进程中mainReactor负责
Proactor
Proactor采用异步IO,将上述Reactor