Reactor,Proactor,Actor

先来说说几种IO模型。

1. 实现层面的网络IO模型

分为:同步阻塞IO,同步非阻塞IO,IO多路复用,异步IO。

1.1 同步阻塞IO

就是linux系统的read和write函数,在调用的时候会被阻塞住,直到完成数据读取或写入。

1.2 同步非阻塞

和上面的API是一样的,只是在打开fd的时候会有一个O_NONBLOCKcanshu。这样当调用read和write的时候,如果没有准备好数据,会立即返回,不会阻塞,然后让应用程序不断去轮询。

1.3 IO多路复用

select、poll、epoll,主要用的是epoll。

1.3.1 select

select本质上是通过设置或检查存放fd标志位的数据结构进行下一步处理。 这带来缺点:

  1. 单个进程可监视的fd数量被限制,即能监听端口的数量有限 单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试 一般该数和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认1024个,64位默认2048。
  2. 对socket是线性扫描,即轮询,效率较低: 仅知道有I/O事件发生,却不知是哪几个流,只会无差异轮询所有流,找出能读数据或写数据的流进行操作。同时处理的流越多,无差别轮询时间越长 - O(n)。
    当socket较多时,每次select都要通过遍历FD_SETSIZE个socket,不管是否活跃,这会浪费很多CPU时间。如果能给 socket 注册某个回调函数,当他们活跃时,自动完成相关操作,即可避免轮询,这就是epoll与kqueue。

1.3.2 poll

select类似,只是描述fd集合的方式不同,poll使用pollfd结构而非select的fd_set结构。 管理多个描述符也是进行轮询,根据描述符的状态进行处理,但poll没有最大文件描述符数量的限制。

poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

它将用户传入的数组拷贝到内核空间
然后查询每个fd对应的设备状态:
如果设备就绪 在设备等待队列中加入一项继续遍历
若遍历完所有fd后,都没发现就绪的设备 挂起当前进程,直到设备就绪或主动超时,被唤醒后它又再次遍历fd。这个过程经历多次无意义的遍历。

没有最大连接数限制,因其基于链表存储,其缺点:

  1. 大量fd数组被整体复制于用户态和内核地址空间间,而不管是否有意义
  2. 如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd
    所以又有了epoll模型。

1.3.3 epoll

epoll模型修改主动轮询为被动通知,当有事件发生时,被动接收通知。所以epoll模型注册套接字后,主程序可做其他事情,当事件发生时,接收到通知后再去处理。

可理解为event poll,epoll会把哪个流发生哪种I/O事件通知我们。所以epoll是事件驱动(每个事件关联fd),此时我们对这些流的操作都是有意义的。复杂度也降到O(1)。

两种触发模式:

  1. LT水平触发:
    读缓冲区只要不为空,就会一直触发读事件;写缓冲区只要不满,就会一直触发写事件。
  2. ET 边缘触发:
    读缓冲区的状态,从空转为非空的时候触发一次,写缓冲区的状态从满转为非满的时候触发一次。
    因此,对于LT和ET,
  3. 对于LT,要避免写的死循环:写缓冲区为满的概率很小,即写的条件会一直满足,所以当用户注册了写事件却没有数据要写时,它会一直触发,因此在LT模式下写完数据一定要取消写事件
  4. 对于ET模式,要避免 short read 问题:例如用户收到100字节,它触发一次,但用户只读到了50字节,剩下的50字节不读,它也不会再次触发。因此在ET模式下,一定要把读缓冲区的数据一次性读完。
    实际开发中,一般倾向于使用LT模式。因为ET一个没处理好,数据就没有了。
    水平和边缘指的是触发时机,水平意思是只要越过0的水平线,就会触发,边缘是只有性质发生变化的时候才会触发一次。

epoll的优点
没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)
效率提升,不是轮询,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数 即Epoll最大的优点就在于它只关心“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll
内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
epoll通过内核和用户空间共享一块内存来实现的
表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现。

select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。

对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)。
对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

1.3.4 总结

select,poll,epoll都是IO多路复用机制,即可以监视多个描述符,一旦某个描述符就绪(读或写就绪),能够通知程序进行相应读写操作。 但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。

1.4 异步IO

指读写都由操作系统完成,然后通过某种机制通知应用程序。windows的IOCP是真正意义上的异步IO。

1.5 总结

PS:

  1. 阻塞和非阻塞是从函数调用的角度来说,同步和异步是从“读写是谁完成的”角度来说的。
  2. 总共有三种:同步阻塞IO,同步非阻塞IO,异步IO。异步IO没有所谓阻塞非阻塞,一定是非阻塞的。
  3. IO多路复用都是同步IO,因为read和write都是应用程序完成的,也是阻塞IO,因为select poll epoll都是阻塞的。

讲IO模型的时候一定要区分清楚是指网络IO模型还是框架IO模型,比如asio和java的NIO,就是基于epoll的IO。

2. Reactor、Proactor和Actor

相对上面几种实现模式,这几种模式更像是逻辑模式。

2.1 Reactor:

主动模式。所谓主动,是指应用程序不断轮询,询问操作系统或者网络框架,IO是否就绪。Linux系统下的select、poll和epoll属于主动模式,需要应用程序中有一个循环一直轮询;Java中的NIO也属于这种模式。在这种模式下,实际的IO操作还是应用程序执行的。
流程:
1 向事件分发器注册事件回调
2 事件发生
4 事件分发器调用之前注册的函数
4 在回调函数中读取数据,对数据进行后续处理
Reactor模型实例:libevent,Redis、ACE

2.2 Proactor:

被动模式。应用程序把read和write函数操作全部交给操作系统或者网络框架,实际的IO操作由操作系统或网络框架完成后,之后再回调应用程序。asio库就是典型的Proactor模式。
异步IO就是Proactor模式。
流程:
1 向事件分发器注册事件回调
2 事件发生
3 操作系统读取数据,并放入应用缓冲区,然后通知事件分发器
4 事件分发器调用之前注册的函数
5 在回调函数中对数据进行后续处理
Preactor模型实例:ASIO

2.3 Reactor和Proactor的主要区别:

reactor和proactor的主要区别是,前者应用在回调函数中读取数据,然后进行后续的数据处理;而后者数据读取有操作系统完成,回调函数制作数据处理。Proactor是异步,Reactor是同步阻塞。

主动和被动
以主动写为例:
Reactor将handle放到select(),等待可写就绪,然后调用write()写入数据;写完处理后续逻辑;
Proactor调用aoi_write后立刻返回,由内核负责写操作,写完后调用相应的回调函数处理后续逻辑;

可以看出,Reactor被动的等待指示事件的到来并做出反应;它有一个等待的过程,做什么都要先放入到监听事件集合中等待handler可用时再进行操作;
Proactor直接调用异步读写操作,调用完后立刻返回;

实现
Reactor实现了一个被动的事件分离和分发模型,服务等待请求事件的到来,再通过不受间断的同步处理事件,从而做出反应;

Proactor实现了一个主动的事件分离和分发模型;这种设计允许多个任务并发的执行,从而提高吞吐量;并可执行耗时长的任务(各个任务间互不影响)

优点
Reactor实现相对简单,对于耗时短的处理场景处理高效;
操作系统可以在多个事件源上等待,并且避免了多线程编程相关的性能开销和编程复杂性;
事件的串行化对应用是透明的,可以顺序的同步执行而不需要加锁;
事务分离:将与应用无关的多路分解和分配机制和与应用相关的回调函数分离开来,

Proactor性能更高,能够处理耗时长的并发场景;

缺点
Reactor处理耗时长的操作会造成事件分发的阻塞,影响到后续事件的处理;

Proactor实现逻辑复杂;依赖操作系统对异步的支持,目前实现了纯异步操作的操作系统少,实现优秀的如windows IOCP,但由于其windows系统用于服务器的局限性,目前应用范围较小;而Unix/Linux系统对纯异步的支持有限,应用事件驱动的主流还是通过select/epoll来实现;

适用场景
Reactor:同时接收多个服务请求,并且依次同步的处理它们的事件驱动程序;
Proactor:异步接收和同时处理多个服务请求的事件驱动程序;

2.4 Actor:

Actor模型被称为高并发事务的终极解决方案,
实体之通过消息通讯,各自处理自己的数据,能够实现这并行。
actor模型实例:skynet,Erlang

Actor模型是一个概念模型,用于处理并发计算。它定义了一系列系统组件应该如何动作和交互的通用规则,最著名的使用这套规则的编程语言是Erlang。

一个Actor指的是一个最基本的计算单元。它能接收一个消息并且基于其执行计算。

这个理念很像面向对象语言,一个对象接收一条消息(方法调用),然后根据接收的消息做事(调用了哪个方法)。

Actors一大重要特征在于actors之间相互隔离,它们并不互相共享内存。这点区别于上述的对象。也就是说,一个actor能维持一个私有的状态,并且这个状态不可能被另一个actor所改变。

思路方向:

其实无论是使用数据库锁 还是多线程,这里有一个共同思路,就是将数据喂给线程,就如同计算机是一套加工流水线,数据作为原材料投入这个流水线的开始,流水线出来后就是成品,这套模式的前提是数据是被动的,自身不复杂,没有自身业务逻辑要求。适合大数据处理或互联网网站应用等等。

但是如果数据自身要求有严格的一致性,也就是事务机制,数据就不能被动被加工,要让数据自己有行为能力保护实现自己的一致性,就像孩子小的时候可以任由爸妈怎么照顾关心都可以,但是如果孩子长大有自己的思想和要求,他就可能不喜欢被爸妈照顾,他要求自己通过行动实现自己的要求。

数据也是如此。

只有我们改变思路,让数据自己有行为维护自己的一致性,才能真正安全实现真正的事务。

我们可以看到

Actor模型=数据+行为+消息。

Actor模型内部的状态由自己的行为维护,外部线程不能直接调用对象的行为,必须通过消息才能激发行为,这样就保证Actor内部数据只有被自己修改。

你可能感兴趣的:(后端编程,网络,网络协议)