传统IO模型:
PPC 是 Process Per Connection 的缩写,其含义是指每次有新的连接就新建一个进程去专门处理这个连接的请求。
TPC shi Thread Per Connection 的缩写,其含义是每次有新的连接就创建一个线程去处理。
缺点:
资源浪费,每次连接就创建一个线程,结束后线程就销毁了
基于以上缺点,我们可以使用线程池进行线程复用,但是如果某个线程阻塞到read 或者是write 上,那么这个连接就一直不会释放,导致其他线程阻塞在获取线程池连接上。
解决这个问题的最简单的方式是将 read 操作改为非阻塞,然后进程不断地轮询多个连接。这种方式能够解决阻塞的问题,但解决的方式并不优雅。首先,轮询是要消耗 CPU 的;其次,如果一个进程处理几千上万的连接,则轮询的效率是很低的。
为了更好地解决上述问题,我们可以当连接上有数据的时候线程采取处理,也就是使用事件监听的模式,如果有数据,你告诉我,然后我去处理,这就是IO多路复用
IO多路复用
-
当多条连接共用一个阻塞对象后,进程只需要在一个阻塞对象上等待,而无须再轮询所有连接,常见的实现方式有 select、epoll、kqueue 等。
-
当某条连接有新的数据可以处理时,操作系统会通知进程,进程从阻塞状态返回,开始进行业务处理
IO 多路复用结合线程池,完美地解决了 PPC 和 TPC 的问题,而且“大神们”给它取了一个很牛的名字:Reactor,中文是“反应堆”。联想到“核反应堆”,听起来就很吓人,实际上这里的“反应”不是聚变、裂变反应的意思,而是“事件反应”的意思,可以通俗地理解为“来了一个事件我就有相应的反应”,这里的“我”就是 Reactor,具体的反应就是我们写的代码,Reactor 会根据事件类型来调用相应的代码进行处理。Reactor 模式也叫 Dispatcher 模式(在很多开源的系统里面会看到这个名称的类,其实就是实现 Reactor 模式的),更加贴近模式本身的含义,即 I/O 多路复用统一监听事件,收到事件后分配(Dispatch)给某个进程。
Reactor
1 reactor 模式:
-
单 Reactor 单进程 / 线程。
-
单 Reactor 多线程。
-
多 Reactor 多进程 / 线程。
2 单 Reactor 单进程 / 线程
- reactor 阻塞在select 上,收到时间后通过dispatch进行分发。
-
如果是连接建立的事件,则由 Acceptor 处理,Acceptor 通过 accept 接受连接,并创建一个 Handler 来处理连接后续的各种事件。
-
如果不是连接建立事件,则 Reactor 会调用连接对应的 Handler(第 2 步中创建的 Handler)来进行响应。
-
Handler 会完成 read-> 业务处理 ->send 的完整业务流程。
缺点:无法发挥出多核优势,如果某个handler阻塞了,那么所有的后续handler都处理不了。
优点:无线程切换,无线程通信,无线程竞争
适用场景:适合于业务处理很快的场景
例子:redis
3 单 Reactor 多线程
相比于单Reactor单进程,handler收到数据并不需要进行业务处理,而是读到数据后,交给processor进行处理。
Processor 会在独立的子线程中完成真正的业务处理,然后将响应结果发给主进程的 Handler 处理;Handler 收到响应后通过 send 将响应结果返回给 client。
缺点:
- 多线程数据共享和访问比较复杂
- Reactor 承担所有事件的监听和响应,只在主线程中运行,瞬间高并发时会成为性能瓶颈
优点:充分利用多核CPU的能力
适用场景:连接不是很多,对高性能有要求
例子:java NIO
4 多 Reactor 多进程 / 线程
-
父进程中 mainReactor 对象通过 select 监控连接建立事件,收到事件后通过 Acceptor 接收,将新的连接分配给某个子进程。
-
子进程的 subReactor 将 mainReactor 分配的连接加入连接队列进行监听,并创建一个 Handler 用于处理连接的各种事件。
-
当有新的事件发生时,subReactor 会调用连接对应的 Handler(即第 2 步中创建的 Handler)来进行响应。
-
Handler 完成 read→业务处理→send 的完整业务流程。
优点:
- 职责明确,父进程只负责接收新连接,具体是连接还是read不关心。然后将连接交给subReactor,subReactor进行处理
- 父子Reactor交互简单
- 子进程之间是独立的
例子:NGINX, Netty,Memcache
nginx实现:master进程不同于一般的主从式reactor(一般的主从式reactor设计会是主reactor负责将连接accept下来,然后再将connection fd挂载到子reactor中),这个master进程的主要任务就是监听信号的,也就是对nginx的一些命令做处理(还记得在之前提到的nginx中的命令处理都是通过信号来处理的吗),然后再将这些处理通过sockerpair()或者信号等方式通知给worker进程,master进程同时监控worker进程的运行状态,当worker进程退出后(异常情况下),会自动重新启动新的worker进程。同时,这个master进程负责listen这个整个服务器的listen fd,然后worker进程通过竞争accept lock来将连接从全连接队列里取出来,nginx也是通过这个竞争accept lock的方式避免的惊群(多个进程同时等待网络的连接事件,当这个事件发生时,这些进程被同时唤醒,就是“惊群”)现象。
Reactor备注:
processor 和handler说明:可以在同一线程进行处理,也可以在不同的线程进行处理。如果有不同的线程进行处理,那么handler线程我们叫做IO线程,也就是只做read 和send操作,processor叫做work线程,由具体的业务方来进行实现,只进行具体的业务处理。但是分开后,会涉及到线程的上下文切换,具体操作需要根据不同情况衡量。
Proactor
Reactor 是非阻塞同步网路模型,即handler执行read / send这类IO操作是同步的,如果把I/O 操作改为异步就能够进一步提升性能,这就是异步网络模型 Proactor。
Proactor 中文翻译为“前摄器”比较难理解,与其类似的单词是 proactive,含义为“主动的”,因此我们照猫画虎翻译为“主动器”反而更好理解。Reactor 可以理解为“来了事件我通知你,你来处理”,而 Proactor 可以理解为“来了事件我来处理,处理完了我通知你”。这里的“我”就是操作系统内核,“事件”就是有新连接、有数据可读、有数据可写的这些 I/O 事件,“你”就是我们的程序代码。
步骤:
-
Proactor Initiator 负责创建 Proactor 和 Handler,并将 Proactor 和 Handler 都通过 Asynchronous Operation Processor 注册到内核。
-
Asynchronous Operation Processor 负责处理注册请求,并完成 I/O 操作。
-
Asynchronous Operation Processor 完成 I/O 操作后通知 Proactor。
-
Proactor 根据不同的事件类型回调不同的 Handler 进行业务处理。
-
Handler 完成业务处理,Handler 也可以注册新的 Handler 到内核进程。
总结:
1 、PPC ,TPC: 假如我们去饭店点餐,饭店人很多,如果我们付了钱后站在收银台等着饭端上来我们才离开,这就成了同步阻塞了。
2、如果我们付了钱后给你一个号就可以离开,饭好了老板会叫号,你过来取。这就是Reactor模型。
3、如果我们付了钱后给我一个号就可以坐到坐位上该干啥干啥,饭好了老板会把饭端上来送给你。这就是Proactor模型了。