这次就来图解 Reactor 和 Proactor 这两个高功能网络形式。
别小看这两个东西,特别是 Reactor 形式,市面上常见的开源软件许多都选用了这个计划,比方 Redis、Nginx、Netty 等等,所以学好这个形式规划的思维,不只要助于咱们了解许多开源软件,并且也能在面试时吹逼。
发车!

演进
假如要让服务器服务多个客户端,那么最直接的办法便是为每一条衔接创立线程。
其实创立进程也是能够的,原理是相同的,进程和线程的差异在于线程比较轻量级些,线程的创立和线程间切换的本钱要小些,为了描绘简述,后边都以线程为例。
处理完事务逻辑后,跟着衔接封闭后线程也相同要毁掉了,可是这样不停地创立和毁掉线程,不只会带来功能开支,也会形成浪费资源,并且假如要衔接几万条衔接,创立几万个线程去应对也是不现实的。
要这么处理这个问题呢?咱们能够运用「资源复用」的办法。
也便是不必再为每个衔接创立线程,而是创立一个「线程池」,将衔接分配给线程,然后一个线程能够处理多个衔接的事务。
不过,这样又引来一个新的问题,线程怎样才干高效地处理多个衔接的事务?
当一个衔接对应一个线程时,线程一般选用「read - 事务处理 - send」的处理流程,假如当时衔接没有数据可读,那么线程会堵塞在 read
操作上( socket 默许状况是堵塞 I/O),不过这种堵塞办法并不影响其他线程。
可是引进了线程池,那么一个线程要处理多个衔接的事务,线程在处理某个衔接的 read
操作时,假如遇到没有数据可读,就会产生堵塞,那么线程就没办法持续处理其他衔接的事务。
要处理这一个问题,最简略的办法便是将 socket 改成非堵塞,然后线程不断地轮询调用 read
操作来判别是否有数据,这种办法尽管该能够处理堵塞的问题,可是处理的办法比较粗犷,由于轮询是要耗费 CPU 的,并且跟着一个 线程处理的衔接越多,轮询的功率就会越低。
上面的问题在于,线程并不知道当时衔接是否有数据可读,然后需求每次经过 read
去打听。
那有没有办法在只要当衔接上有数据的时分,线程才去建议读恳求呢?答案是有的,完结这一技能的便是 I/O 多路复用。
I/O 多路复用技能会用一个体系调用函数来监听咱们一切关怀的衔接,也就说能够在一个监控线程里边监控许多的衔接。

咱们了解的 select/poll/epoll 便是内核提供给用户态的多路复用体系调用,线程能够经过一个体系调用函数从内核中获取多个作业。
PS:假如想知道 select/poll/epoll 的差异,能够看看小林之前写的这篇文章:这次容许我,一举拿下 I/O 多路复用!
select/poll/epoll 是怎么获取网络作业的呢?
在获取作业时,先把咱们要关怀的衔接传给内核,再由内核检测:
-
假如没有作业产生,线程只需堵塞在这个体系调用,而无需像前面的线程池计划那样轮训调用 read 操作来判别是否有数据。
-
假如有作业产生,内核会回来产生了作业的衔接,线程就会从堵塞状态回来,然后在用户态中再处理这些衔接对应的事务即可。
当下开源软件能做到网络高功能的原因便是 I/O 多路复用吗?
是的,基本是依据 I/O 多路复用,用过 I/O 多路复用接口写网络程序的同学,必定知道是面向进程的办法写代码的,这样的开发的功率不高。
大佬们还为这种形式取了个让人第一时刻难以了解的姓名:Reactor 形式。
Reactor 翻译过来的意思是「反响堆」,或许咱们会联想到物理学里的核反响堆,实践上并不是的这个意思。
这儿的反响指的是「对作业反响」,也便是来了一个作业,Reactor 就有相对应的反响/呼应。
事实上,Reactor 形式也叫 Dispatcher
形式,我觉得这个姓名更贴合该形式的意义,即 I/O 多路复用监听作业,收到作业后,依据作业类型分配(Dispatch)给某个进程 / 线程。
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
单 Reactor 单进程 / 线程
一般来说,C 言语完结的是「单 Reactor 单进程」的计划,由于 C 语编写完的程序,运转后便是一个独立的进程,不需求在进程中再创立线程。
而 Java 言语完结的是「单 Reactor 单线程」的计划,由于 Java 程序是跑在 Java 虚拟机这个进程上面的,虚拟机中有许多线程,咱们写的 Java 程序只是其间的一个线程罢了。
咱们来看看「单 Reactor 单进程」的计划示意图:

能够看到进程里有 Reactor、Acceptor、Handler 这三个目标:
-
Reactor 目标的作用是监听和分发作业;
-
Acceptor 目标的作用是获取衔接;
-
Handler 目标的作用是处理事务;
目标里的 select、accept、read、send 是体系调用函数,dispatch 和 「事务处理」是需求完结的操作,其间 dispatch 是分发作业操作。
接下来,介绍下「单 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 单线程 / 进程」计划的缺点,那么就需求引进多线程 / 多进程,这样就产生了单 Reactor 多线程 / 多进程的计划。
闻其名不如看其图,先来看看「单 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 多进程 / 线程
要处理「单 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 多进程有些差异。
Proactor
前面说到的 Reactor 对错堵塞同步网络形式,而 Proactor 是异步网络形式。
这儿先给咱们温习下堵塞、非堵塞、同步、异步 I/O 的概念。
先来看看堵塞 I/O,当用户程序履行 read
,线程会被堵塞,一向比及内核数据预备好,并把数据从内核缓冲区复制到运用程序的缓冲区中,当复制进程完结,read
才会回来。
留意,堵塞等候的是「内核数据预备好」和「数据从内核态复制到用户态」这两个进程。进程如下图:

知道了堵塞 I/O ,来看看非堵塞 I/O,非堵塞的 read 恳求在数据未预备好的状况下当即回来,能够持续往下履行,此刻运用程序不断轮询内核,直到数据预备好,内核将数据复制到运用程序缓冲区,read
调用才干够获取到成果。进程如下图:

留意,这儿最终一次 read 调用,获取数据的进程,是一个同步的进程,是需求等候的进程。这儿的同步指的是内核态的数据复制到用户程序的缓存区这个进程。
举个比方,假如 socket 设置了 O_NONBLOCK
标志,那么就表明运用的对错堵塞 I/O 的办法拜访,而不做任何设置的话,默许是堵塞 I/O。
因而,不管 read 和 send 是堵塞 I/O,仍对错堵塞 I/O 都是同步调用。由于在 read 调用时,内核将数据从内核空间复制到用户空间的进程都是需求等候的,也便是说这个进程是同步的,假如内核完结的复制功率不高,read 调用就会在这个同步进程中等候比较长的时刻。
而真实的异步 I/O 是「内核数据预备好」和「数据从内核态复制到用户态」这两个进程都不必等候。
当咱们建议 aio_read
(异步 I/O) 之后,就当即回来,内核自动将数据从内核空间复制到用户空间,这个复制进程相同是异步的,内核自动完结的,和前面的同步操作不相同,运用程序并不需求自动建议复制动作。进程如下图:

举个你去饭堂吃饭的比方,你比方运用程序,饭堂比方操作体系。
堵塞 I/O 比方,你去饭堂吃饭,可是饭堂的菜还没做好,然后你就一向在那里等啊等,等了好长一段时刻总算比及饭堂阿姨把菜端了出来(数据预备的进程),可是你还得持续等阿姨把菜(内核空间)打到你的饭盒里(用户空间),阅历完这两个进程,你才干够脱离。
非堵塞 I/O 比方,你去了饭堂,问阿姨菜做好了没有,阿姨告知你没,你就脱离了,过几十分钟,你又来饭堂问阿姨,阿姨说做好了,所以阿姨帮你把菜打到你的饭盒里,这个进程你是得等候的。
异步 I/O 比方,你让饭堂阿姨将菜做好并把菜打到饭盒里后,把饭盒送到你面前,整个进程你都不需求任何等候。
很明显,异步 I/O 比同步 I/O 功能更好,由于异步 I/O 在「内核数据预备好」和「数据从内核空间复制到用户空间」这两个进程都不必等候。
Proactor 正是选用了异步 I/O 技能,所以被称为异步网络模型。
现在咱们再来了解 Reactor 和 Proactor 的差异,就比较明晰了。
-
Reactor 对错堵塞同步网络形式,感知的是安排妥当可读写作业。在每次感知到有作业产生(比方可读安排妥当作业)后,就需求运用进程自动调用 read 办法来完结数据的读取,也便是要运用进程自动将 socket 接纳缓存中的数据读到运用进程内存中,这个进程是同步的,读取完数据后运用进程才干处理数据。
-
Proactor 是异步网络形式, 感知的是已完结的读写作业
因而,Reactor 能够了解为「来了作业操作体系告诉运用进程,让运用进程来处理」,而 Proactor 能够了解为「来了作业操作体系来处理,处理完再告诉运用进程」。这儿的「作业」便是有新衔接、有数据可读、有数据可写的这些 I/O 作业这儿的「处理」包括从驱动读取到内核以及从内核读取到用户空间。
不管是 Reactor,仍是 Proactor,都是一种依据「作业分发」的网络编程形式,差异在于 Reactor 形式是依据「待完结」的 I/O 作业,而 Proactor 形式则是依据「已完结」的 I/O 作业。
接下来,一同看看 Proactor 形式的示意图:

介绍一下 Proactor 形式的作业流程:
-
Proactor Initiator 担任创立 Proactor 和 Handler 目标,并将 Proactor 和 Handler 都经过
Asynchronous Operation Processor 注册到内核; -
Asynchronous Operation Processor 担任处理注册恳求,并处理 I/O 操作;
-
Asynchronous Operation Processor 完结 I/O 操作后告诉 Proactor;
-
Proactor 依据不同的作业类型回调不同的 Handler 进行事务处理;
-
Handler 完结事务处理;
惋惜的是,在 Linux 下的异步 I/O 是不完善的,aio
系列函数是由 POSIX 界说的异步操作接口,不是真实的操作体系等级支撑的,而是在用户空间模仿出来的异步,并且只是支撑依据本地文件的 aio 异步操作,网络编程中的 socket 是不支撑的,这也使得依据 Linux 的高功能网络程序都是运用 Reactor 计划。
而 Windows 里完结了一套完好的支撑 socket 的异步编程接口,这套接口便是 IOCP
,是由操作体系等级完结的异步 I/O,真实意义上异步 I/O,因而在 Windows 里完结高功能网络程序能够运用功率更高的 Proactor 计划。
总结
常见的 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 作业。
-
https://cloud.tencent.com/developer/article/1373468
-
-
https://time.geekbang.org/column/article/8805
-
https://www.cnblogs.com/crazymakercircle/p/9833847.html
学废的小伙伴,点个赞
让我看看有多少人学会啦!