Reactor, Dispatcher 模式
Contents
Reactor / Dispatcher 模式
了解 Reactor 模式,就要先从事件驱动的开发方式说起。
我们知道,服务器开发,CPU 的处理速度远高于 IO 速度,为了避免 CPU 因为 IO 而阻塞,好一点的方法是多进程或线程处理,但这会带来一些进程切换的开销。
这时先驱者找到了事件驱动,或者叫回调的方法。这种方式就是,应用向一个中间人注册一个回调 (Event handler),当 IO 就绪后,这个中间人产生一个事件,并通知此 handler 进行处理。这种回调的方式,也实现了"好莱坞原则” - “Don’t call us, we’ll call you.”
那在 IO 就绪这个事件后,谁来充当这个中间人?Reactor 模式的答案是: 有一个不断等待和循环的单独进程 (线程) 来做这件事,它接受所有 handler 的注册,并负责向操作系统查询 IO 是否就绪,在就绪后用指定的 handler 进行处理,这个角色的名称就叫做 Reactor。
事件驱动, 回调函数, reactor, 响应式编程
事件驱动是概念,回调函数是实现方式。不用回调函数,也可以实现事件驱动。例如:把事件消息发送到队列,另外一个进程取队列处理即可 (没有回调函数)。事件驱动的本质特征:中心轮询机制。event loop的loop是轮询。轮询的目的是什么?感知!对象发生变化,如何感知这种变化?不断的循环查询,loop探测!系统n个对象,每个对象一个for循环 探测彼此的变化?nonono……建立一个轮询中心,这个轮询中心去轮询每个对象,这就是事件驱动。发生了变化,通知感兴趣的对象,怎么处理?就是定义一个回调函数。事件驱动,属于“感知层”的概念;轮询中心,往往就是操作系统本身;对于浏览器而言,就是浏览器本身。也就是系统是轮询中心,你定义 函数,系统调用你定义的函数。对比:系统定义api,你调用api。谁定义函数,谁调用,角色颠倒了!api:系统定义的函数,你去调用;
事件驱动:你定义的回调函数,被系统调用。还是没有懂?事件驱动,就是“哨兵模式”!哨兵轮询环境信息,你就安心睡大觉好了,不用每个人都轮询环境。发生了事件,哨兵 (操作系统/浏览器/轮询中心)负责通知你!怎么处理这个消息,是你的责任!
作者:fdego 链接:https://www.zhihu.com/question/30396023/answer/447966119 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
事件驱动, reactor
The reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by one or more inputs. https://en.wikipedia.org/wiki/Reactor_pattern
响应式 和 事件驱动编程
用过两个编程之后就会觉得他们非常相近, 两个代表作分别是 JS 的 eventEmitter 和 Rxjs
了解之后 reactive programming (响应式) 其实是基于 event data programing (事件驱动) 的方式来处理 数据流,
Event data programming: 当某个件事情发生我再做某些事情
Reactive programming: 当某个件事情发生事件告诉我, 在这之前你可以吧数据处理好先
Event data programming 和 Reactive programming 区别在于:
Event data programming 事件是全局性的当发出一个信号大家都会听看看是不是自己的, reactive programing 每个事件是唯一的, 如果你想监听这个事件你需要订阅它, 在这方面感觉 reactive 极大优化了性能. Event data programing 只处理事件, Reactive programming 除了可以订阅事件还可以订阅某个数据变化. https://cnodejs.org/topic/5ccdb79d776fb66e0d171c2a
Reactor
在获取事件时,先把我们要关心的连接传给内核,再由内核检测: 如果没有事件发生,线程只需阻塞在这个系统调用,而无需像线程池方案那样轮训调用 read 操作来判断是否有数据。如果有事件发生,内核会返回产生了事件的连接,线程就会从阻塞状态返回,然后在用户态中再处理这些连接对应的业务即可。
这种模式叫做 Reactor 模式。
Reactor 模式主要由 Reactor 和处理资源池这两个核心部分组成,它俩负责的事情如下: Reactor 负责监听和分发事件,事件类型包含连接事件、读写事件;处理资源池负责处理事件,如 read -> 业务逻辑 -> send;Reactor 模式是灵活多变的,可以应对不同的业务场景,灵活在于: Reactor 的数量可以只有一个,也可以有多个;处理资源池可以是单个进程 / 线程,也可以是多个进程/线程;将上面的两个因素排列组设一下,理论上就可以有 4 种方案选择: 单 Reactor 单进程 / 线程;单 Reactor 多进程 / 线程;多 Reactor 单进程 / 线程;多 Reactor 多进程 / 线程;其中,「多 Reactor 单进程 / 线程」实现方案相比「单 Reactor 单进程 / 线程」方案,不仅复杂而且也没有性能优势,因此实际中并没有应用。剩下的 3 个方案都是比较经典的,且都有应用在实际的项目中: 单 Reactor 单进程 / 线程;单 Reactor 多线程 / 进程;多 Reactor 多进程 / 线程;方案具体使用进程还是线程,要看使用的编程语言以及平台有关: Java 语言一般使用线程,比如 Netty;C 语言使用进程和线程都可以,例如 Nginx 使用的是进程,Memcache 使用的是线程。
单 Reactor 单进程
Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。单 Reactor 单进程的方案因为全部工作都在同一个进程内完成,所以实现起来比较简单,不需要考虑进程间通信,也不用担心多进程竞争。但是,这种方案存在 2 个缺点: 第一个缺点,因为只有一个进程,无法充分利用 多核 CPU 的性能;第二个缺点,Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的,如果业务处理耗时比较长,那么就造成响应的延迟;所以,单 Reactor 单进程的方案不适用计算密集型的场景,只适用于业务处理非常快速的场景。Redis 是由 C 语言实现的,它采用的正是「单 Reactor 单进程」的方案,因为 Redis 业务处理主要是在内存中完成,操作的速度是很快的,性能瓶颈不在 CPU 上,所以 Redis 对于命令的处理是单进程的方案。
单 Reactor 多线程 / 多进程
Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;上面的三个步骤和单 Reactor 单线程方案是一样的,接下来的步骤就开始不一样了: Handler 对象不再负责业务处理,只负责数据的接收和发送,Handler 对象通过 read 读取到数据后,会将数据发给子线程里的 Processor 对象进行业务处理;子线程里的 Processor 对象就进行业务处理,处理完后,将结果发给主线程中的 Handler 对象,接着由 Handler 通过 send 方法将响应结果发送给 client;单 Reator 多线程的方案优势在于能够充分利用多核 CPU 的能,那既然引入多线程,那么自然就带来了多线程竞争资源的问题。例如,子线程完成业务处理后,要把结果传递给主线程的 Reactor 进行发送,这里涉及共享数据的竞争。要避免多线程由于竞争共享资源而导致数据错乱的问题,就需要在操作共享资源前加上互斥锁,以保证任意时间里只有一个线程在操作共享资源,待该线程操作完释放互斥锁后,其他线程才有机会操作共享数据。聊完单 Reactor 多线程的方案,接着来看看单 Reactor 多进程的方案。事实上,单 Reactor 多进程相比单 Reactor 多线程实现起来很麻烦,主要因为要考虑子进程 <-> 父进程的双向通信,并且父进程还得知道子进程要将数据发送给哪个客户端。而多线程间可以共享数据,虽然要额外考虑并发问题,但是这远比进程间通信的复杂度低得多,因此实际应用中也看不到单 Reactor 多进程的模式。另外,「单 Reactor」的模式还有个问题,因为一个 Reactor 对象承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。
多 Reactor 多进程 / 线程
主线程中的 MainReactor 对象通过 select 监控连接建立事件,收到事件后通过 Acceptor 对象中的 accept 获取连接,将新的连接分配给某个子线程;子线程中的 SubReactor 对象将 MainReactor 对象分配的连接加入 select 继续进行监听,并创建一个 Handler 用于处理连接的响应事件。如果有新的事件发生时,SubReactor 对象会调用当前连接对应的 Handler 对象来进行响应。Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。多 Reactor 多线程的方案虽然看起来复杂的,但是实际实现时比单 Reactor 多线程的方案要简单的多,原因如下: 主线程和子线程分工明确,主线程只负责接收新连接,子线程负责完成后续的业务处理。主线程和子线程的交互很简单,主线程只需要把新连接传给子线程,子线程无须返回数据,直接就可以在子线程将处理结果发送给客户端。大名鼎鼎的两个开源软件 Netty 和 Memcache 都采用了「多 Reactor 多线程」的方案。采用了「多 Reactor 多进程」方案的开源软件是 Nginx,不过方案与标准的多 Reactor 多进程有些差异。具体差异表现在主进程中仅仅用来初始化 socket,并没有创建 mainReactor 来 accept 连接,而是由子进程的 Reactor 来 accept 连接,通过锁来控制一次只有一个子进程进行 accept (防止出现惊群现象) ,子进程 accept 新连接后就放到自己的 Reactor 进行处理,不会再分配给其他子进程。
阻塞、非阻塞、同步、异步 I/O
先来看看阻塞 I/O,当用户程序执行 read, 线程会被阻塞,一直等到内核数据准备好,并把数据从内核缓冲区拷贝到应用程序的缓冲区中,当拷贝过程完成,read 才会返回。
注意,阻塞等待的是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程。
非阻塞的 read 请求在数据未准备好的情况下立即返回,可以继续往下执行,此时应用程序不断轮询内核,直到数据准备好,内核将数据拷贝到应用程序缓冲区,read 调用才可以获取到结果。 最后一次 read 调用,获取数据的过程,是一个同步的过程,是需要等待的过程。这里的同步指的是内核态的数据拷贝到用户程序的缓存区这个过程。
而真正的异步 I/O 是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待。当我们发起 aio_read (异步 I/O) 之后,就立即返回,内核自动将数据从内核空间拷贝到用户空间,这个拷贝过程同样是异步的,内核自动完成的,和前面的同步操作不一样,应用程序并不需要主动发起拷贝动作。
阻塞 I/O 好比,你去饭堂吃饭,但是饭堂的菜还没做好,然后你就一直在那里等啊等,等了好长一段时间终于等到饭堂阿姨把菜端了出来 (数据准备的过程) ,但是你还得继续等阿姨把菜 (内核空间) 打到你的饭盒里 (用户空间) ,经历完这两个过程,你才可以离开。非阻塞 I/O 好比,你去了饭堂,问阿姨菜做好了没有,阿姨告诉你没,你就离开了,过几十分钟,你又来饭堂问阿姨,阿姨说做好了,于是阿姨帮你把菜打到你的饭盒里,这个过程你是得等待的。异步 I/O 好比,你让饭堂阿姨将菜做好并把菜打到饭盒里后,把饭盒送到你面前,整个过程你都不需要任何等待。很明显,异步 I/O 比同步 I/O 性能更好,因为异步 I/O 在「内核数据准备好」和「数据从内核空间拷贝到用户空间」这两个过程都不用等待。
proactor
Proactor 正是采用了异步 I/O 技术,所以被称为异步网络模型。
Reactor 是同步非阻塞网络模式,感知的是就绪可读写事件。在每次感知到有事件发生 (比如可读就绪事件) 后,就需要应用进程主动调用 read 方法来完成数据的读取,也就是要应用进程主动将 socket 接收缓存中的数据读到应用进程内存中,这个过程是同步的,读取完数据后应用进程才能处理数据。Proactor 是异步网络模式, 感知的是已完成的读写事件。在发起异步读写请求时,需要传入数据缓冲区的地址 (用来存放结果数据) 等信息,这样系统内核才可以自动帮我们把数据的读写工作完成,这里的读写工作全程由操作系统来做,并不需要像 Reactor 那样还需要应用进程主动发起 read/write 来读写数据,操作系统完成读写工作后,就会通知应用进程直接处理数据。
总结常见的 Reactor 实现方案有三种。
- 第一种方案单 Reactor 单进程 / 线程,不用考虑进程间通信以及数据同步的问题,因此实现起来比较简单,这种方案的缺陷在于无法充分利用多核 CPU,而且处理业务逻辑的时间不能太长,否则会延迟响应,所以不适用于计算机密集型的场景,适用于业务处理快速的场景,比如 Redis 采用的是单 Reactor 单进程的方案。
- 第二种方案单 Reactor 多线程,通过多线程的方式解决了方案一的缺陷,但它离高并发还差一点距离,差在只有一个 Reactor 对象来承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。
- 第三种方案多 Reactor 多进程 / 线程,通过多个 Reactor 来解决了方案二的缺陷,主 Reactor 只负责监听事件,响应事件的工作交给了从 Reactor,Netty 和 Memcache 都采用了「多 Reactor 多线程」的方案,Nginx 则采用了类似于 「多 Reactor 多进程」的方案。Reactor 可以理解为「来了事件操作系统通知应用进程,让应用进程来处理」,而 Proactor 可以理解为「来了事件操作系统来处理,处理完再通知应用进程」。因此,真正的大杀器还是 Proactor,它是采用异步 I/O 实现的异步网络模型,感知的是已完成的读写事件,而不需要像 Reactor 感知到事件后,还需要调用 read 来从内核中获取数据。不过,无论是 Reactor,还是 Proactor,都是一种基于「事件分发」的网络编程模式,区别在于 Reactor 模式是基于「待完成」的 I/O 事件,而 Proactor 模式则是基于「已完成」的 I/O 事件。
Reactor, Observer
Reactor 模式与 Observer 模式在某些方面极为相似: 当一个主体发生改变时,所有依属体都得到通知。不过,观察者模式与单个事件源关联,而反应器模式则与多个事件源关联 。
最最原始的网络编程思路就是服务器用一个while循环,不断监听端口是否有新的 socket 连接,如果有,那么就调用一个处理函数处理,类似:
|
|
这种方法的最大问题是无法并发,效率太低,如果当前的请求没有处理完,那么后面的请求只能被阻塞,服务器的吞吐量太低。 之后,想到了使用多线程,也就是很经典的connection per thread,每一个连接用一个线程处理,类似:
|
|
tomcat服务器的早期版本确实是这样实现的。多线程的方式确实一定程度上极大地提高了服务器的吞吐量,因为之前的请求在read阻塞以后,不会影响到后续的请求,因为他们在不同的线程中。这也是为什么通常会讲“一个线程只能对应一个socket”的原因。最开始对这句话很不理解,线程中创建多个socket不行吗?语法上确实可以,但是实际上没有用,每一个socket都是阻塞的,所以在一个线程里只能处理一个socket,就算accept了多个也没用,前一个socket被阻塞了,后面的是无法被执行到的。 缺点在于资源要求太高,系统中创建线程是需要比较高的系统资源的,如果连接数太高,系统无法承受,而且,线程的反复创建-销毁也需要代价。 线程池本身可以缓解线程创建-销毁的代价,这样优化确实会好很多,不过还是存在一些问题的,就是线程的粒度太大。每一个线程把一次交互的事情全部做了,包括读取和返回,甚至连接,表面上似乎连接不在线程里,但是如果线程不够,有了新的连接,也无法得到处理,所以,目前的方案线程里可以看成要做三件事,连接,读取和写入。 线程同步的粒度太大了,限制了吞吐量。应该把一次连接的操作分为更细的粒度或者过程,这些更细的粒度是更小的线程。整个线程池的数目会翻倍,但是线程更简单,任务更加单一。这其实就是Reactor出现的原因,在Reactor中,这些被拆分的小线程或者子过程对应的是handler,每一种handler会出处理一种event。这里会有一个全局的管理者selector,我们需要把channel注册感兴趣的事件,那么这个selector就会不断在channel上检测是否有该类型的事件发生,如果没有,那么主线程就会被阻塞,否则就会调用相应的事件处理函数即handler来处理。典型的事件有连接,读取和写入,当然我们就需要为这些事件分别提供处理器,每一个处理器可以采用线程的方式实现。一个连接来了,显示被读取线程或者handler处理了,然后再执行写入,那么之前的读取就可以被后面的请求复用,吞吐量就提高了
Reactor 翻译过来的意思是「反应堆」,可能大家会联想到物理学里的核反应堆,实际上并不是的这个意思。这里的反应指的是「对事件反应」,也就是来了一个事件,Reactor 就有相对应的反应/响应。事实上,Reactor 模式也叫 Dispatcher 模式,我觉得这个名字更贴合该模式的含义,即 I/O 多路复用监听事件,收到事件后,根据事件类型分配 (Dispatch) 给某个进程 / 线程。Reactor 模式主要由 Reactor 和处理资源池这两个核心部分组成,它俩负责的事情如下: Reactor 负责监听和分发事件,事件类型包含连接事件、读写事件;处理资源池负责处理事件,如 read -> 业务逻辑 -> send;Reactor 模式是灵活多变的,可以应对不同的业务场景,灵活在于: Reactor 的数量可以只有一个,也可以有多个;处理资源池可以是单个进程 / 线程,也可以是多个进程 /线程;
作者: 小林coding 链接: https://www.zhihu.com/question/26943938/answer/1856426252 来源: 知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Reactor模式也叫反应器模式,大多数IO相关组件如Netty、Redis在使用的IO模式 最最原始的网络编程思路就是服务器用一个while循环,不断监听端口是否有新的 socket 连接,如果有,那么就调用一个处理函数处理,类似:
while(true){
socket = accept();
handle(socket)
} 这种方法的最大问题是无法并发,效率太低,如果当前的请求没有处理完,那么后面的请求只能被阻塞,服务器的吞吐量太低。
之后,想到了使用多线程,也就是很经典的connection per thread,每一个连接用一个线程处理,类似:
单 reactor 单线程
单 reactor 多线程
多 reactor 多线程
Netty中的Reactor与NIO
好了,了解了多线程下的Reactor模式,我们来看看Netty吧 (以下部分主要针对NIO,OIO部分更加简单一点,不重复介绍了)。Netty里对应mainReactor的角色叫做“Boss”,而对应subReactor的角色叫做”Worker”。Boss负责分配请求,Worker负责执行,好像也很贴切!以TCP的Server端为例,这两个对应的实现类分别为NioServerBoss和NioWorker (Server和Client的Worker没有区别,因为建立连接之后,双方就是对等的进行传输了)。
Netty 3.7中Reactor的EventLoop在AbstractNioSelector.run()中,它实现了Runnable接口。这个类是Netty NIO部分的核心。它的逻辑非常复杂,其中还包括一些对JDK Bug的处理 (例如rebuildSelector)
https://www.cnblogs.com/crazymakercircle/p/9833847.html
http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
http://www.linkedkeeper.com/12.html http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
Author -
LastMod 2021-07-11