"反应" 器名字中 "反应" 的由来:
"反应" 即 "倒置","控制逆转", 具体事件处理程序不调用反应器, 而向反应器注册一个事件处理器, 表示自己对某些事件感兴趣, 有时间来了, 具体事件处理程序通过事件处理器对某个指定的事件发生做出反应; 这种控制逆转又称为 "好莱坞法则"(不要调用我, 让我来调用你)
例如, 路人甲去做男士 SPA, 前台的接待小姐接待了路人甲, 路人甲现在只对 10000 技师感兴趣, 就告诉接待小姐, 等 10000 技师上班了或者是空闲了, 通知我. 等路人甲接到通知了, 做出了反应, 把 10000 技师占住了, 然后, 路人甲想起上一次的那个 10000 号房间不错, 设备舒适, 灯光暧昧, 又告诉前台的接待小姐, 我对 10000 号房间很感兴趣, 房间空出来了就告诉我, 我现在先和 10000 这个小姐聊下人生, 10000 号房间空出来了, 路人甲接到通知了, 路人甲再次做出了反应. 路人甲就是具体事件处理程序, 前台的接待小姐就是所谓的反应器, 10000 技师和 10000 号房间空闲了就是事件, 路人甲只对这两个事件感兴趣, 其他, 比如 10001 号技师或者 10002 号房间空闲了也是事件, 但是路人甲不感兴趣.
单线程 Reactor 模式流程:
1 服务器端的 Reactor 是一个线程对象, 该线程会启动事件循环, 并使用 Selector(选择器)来实现 IO 的多路复用. 注册一个 Acceptor 事件处理器到 Reactor 中, Acceptor 事件处理器所关注的事件是 ACCEPT 事件, 这样 Reactor 会监听客户端向服务器端发起的连接请求事件(ACCEPT 事件).
2 客户端向服务器端发起一个连接请求, Reactor 监听到了该 ACCEPT 事件的发生并将该 ACCEPT 事件派发给相应的 Acceptor 处理器来进行处理. Acceptor 处理器通过 accept()方法得到与这个客户端对应的连接(SocketChannel), 然后将该连接所关注的 READ 事件以及对应的 READ 事件处理器注册到 Reactor 中, 这样一来 Reactor 就会监听该连接的 READ 事件了.
3 当 Reactor 监听到有读或者写事件发生时, 将相关的事件派发给对应的处理器进行处理. 比如, 读处理器会通过 SocketChannel 的 read()方法读取数据, 此时 read()操作可以直接读取到数据, 而不会堵塞与等待可读的数据到来.
4 每当处理完所有就绪的感兴趣的 I/O 事件后, Reactor 线程会再次执行 select()阻塞等待新的事件就绪并将其分派给对应处理器进行处理.
注意, Reactor 的单线程模式的单线程主要是针对于 I/O 操作而言, 也就是所有的 I/O 的 accept(),read(),write()以及 connect()操作都在一个线程上完成的.
但在目前的单线程 Reactor 模式中, 不仅 I/O 操作在该 Reactor 线程上, 连非 I/O 的业务操作也在该线程上进行处理了, 这可能会大大延迟 I/O 请求的响应. 所以我们应该将非 I/O 的业务逻辑操作从 Reactor 线程上卸载, 以此来加速 Reactor 线程对 I/O 请求的响应.
单线程 Reactor, 工作者线程池
与单线程 Reactor 模式不同的是, 添加了一个工作者线程池, 并将非 I/O 操作从 Reactor 线程中移出转交给工作者线程池来执行. 这样能够提高 Reactor 线程的 I/O 响应, 不至于因为一些耗时的业务逻辑而延迟对后面 I/O 请求的处理.
使用线程池的优势:
1 通过重用现有的线程而不是创建新线程, 可以在处理多个请求时分摊在线程创建和销毁过程产生的巨大开销.
2 另一个额外的好处是, 当请求到达时, 工作线程通常已经存在, 因此不会由于等待创建线程而延迟任务的执行, 从而提高了响应性.
3 通过适当调整线程池的大小, 可以创建足够多的线程以便使处理器保持忙碌状态. 同时还可以防止过多线程相互竞争资源而使应用程序耗尽内存或失败.
改进的版本中, 所以的 I/O 操作依旧由一个 Reactor 来完成, 包括 I/O 的 accept(),read(),write()以及 connect()操作.
对于一些小容量应用场景, 可以使用单线程模型. 但是对于高负载, 大并发或大数据量的应用场景却不合适, 主要原因如下:
1 一个 NIO 线程同时处理成百上千的链路, 性能上无法支撑, 即便 NIO 线程的 CPU 负荷达到 100%, 也无法满足海量消息的读取和发送;
2 当 NIO 线程负载过重之后, 处理速度将变慢, 这会导致大量客户端连接超时, 超时之后往往会进行重发, 这更加重了 NIO 线程的负载, 最终会导致大量消息积压和处理超时, 成为系统的性能瓶颈;
多 Reactor 线程模式
Reactor 线程池中的每一 Reactor 线程都会有自己的 Selector, 线程和分发的事件循环逻辑.
mainReactor 可以只有一个, 但 subReactor 一般会有多个. mainReactor 线程主要负责接收客户端的连接请求, 然后将接收到的 SocketChannel 传递给 subReactor, 由 subReactor 来完成和客户端的通信.
流程:
1 注册一个 Acceptor 事件处理器到 mainReactor 中, Acceptor 事件处理器所关注的事件是 ACCEPT 事件, 这样 mainReactor 会监听客户端向服务器端发起的连接请求事件(ACCEPT 事件). 启动 mainReactor 的事件循环.
2 客户端向服务器端发起一个连接请求, mainReactor 监听到了该 ACCEPT 事件并将该 ACCEPT 事件派发给 Acceptor 处理器来进行处理. Acceptor 处理器通过 accept()方法得到与这个客户端对应的连接(SocketChannel), 然后将这个 SocketChannel 传递给 subReactor 线程池.
3 subReactor 线程池分配一个 subReactor 线程给这个 SocketChannel, 即, 将 SocketChannel 关注的 READ 事件以及对应的 READ 事件处理器注册到 subReactor 线程中. 当然你也注册 WRITE 事件以及 WRITE 事件处理器到 subReactor 线程中以完成 I/O 写操作. Reactor 线程池中的每一 Reactor 线程都会有自己的 Selector, 线程和分发的循环逻辑.
4 当有 I/O 事件就绪时, 相关的 subReactor 就将事件派发给响应的处理器处理. 注意, 这里 subReactor 线程只负责完成 I/O 的 read()操作, 在读取到数据后将业务逻辑的处理放入到线程池中完成, 若完成业务逻辑后需要返回数据给客户端, 则相关的 I/O 的 write 操作还是会被提交回 subReactor 线程来完成.
注意, 所以的 I/O 操作 (包括, I/O 的 accept(),read(),write() 以及 connect()操作)依旧还是在 Reactor 线程 (mainReactor 线程 或 subReactor 线程) 中完成的. Thread Pool(线程池)仅用来处理非 I/O 操作的逻辑.
多 Reactor 线程模式将 "接受客户端的连接请求" 和 "与该客户端的通信" 分在了两个 Reactor 线程来完成. mainReactor 完成接收客户端连接请求的操作, 它不负责与客户端的通信, 而是将建立好的连接转交给 subReactor 线程来完成与客户端的通信, 这样一来就不会因为 read()数据量太大而导致后面的客户端连接请求得不到即时处理的情况. 并且多 Reactor 线程模式在海量的客户端并发请求的情况下, 还可以通过实现 subReactor 线程池来将海量的连接分发给多个 subReactor 线程, 在多核的操作系统中这能大大提升应用的负载和吞吐量.
Netty 服务端使用了 "多 Reactor 线程模式"
和观察者模式的区别
观察者模式:
也可以称为为 发布 - 订阅 模式, 主要适用于多个对象依赖某一个对象的状态并, 当某对象状态发生改变时, 要通知其他依赖对象做出更新. 是一种一对多的关系. 当然, 如果依赖的对象只有一个时, 也是一种特殊的一对一关系. 通常, 观察者模式适用于消息事件处理, 监听者监听到事件时通知事件处理者对事件进行处理(这一点上面有点像是回调, 容易与反应器模式和前摄器模式的回调搞混淆).
Reactor 模式:
reactor 模式, 即反应器模式, 是一种高效的异步 IO 模式, 特征是回调, 当 IO 完成时, 回调对应的函数进行处理. 这种模式并非是真正的异步, 而是运用了异步的思想, 当 IO 事件触发时, 通知应用程序作出 IO 处理. 模式本身并不调用系统的异步 IO 函数.
reactor 模式与观察者模式有点像. 不过, 观察者模式与单个事件源关联, 而反应器模式则与多个事件源关联 . 当一个主体发生改变时, 所有依属体都得到通知.
来源: http://www.bubuko.com/infodetail-3117891.html