简介
Reactor和Proactor都是基于事件的并发模型。除了基于事件外,还有基于线程的并发模型。这里先简单介绍下两种的特点以及区别。
基于线程的模型
主要有两种模型:
- 每个客户端的连接分配一个线程,每个线程独立的处理连接。
- 有一个线程池,每个客户端的生命周期都拥有一个线程。
可以看出,这两种方式的核心思想都是,每个连接,都独立地拥有一个线程,这个线程伴随着该连接的整个生命周期。
这种方式的缺陷在于,线程本身的资源消耗是昂贵的;唯一的优势是,管理方便,适合少量的连接。但是现在后端高并发的情况下,这种基于线程的模型肯定不适用。不过,Golang的轻量级协程却适合这种场景,为每个连接分配一个协程。
基于事件的模型
连接建立后,一般都是用socket
的形式管理,有这种思想:连接在大多数情况下,都是空闲的,即不需要CPU的时间片段,我们可以让那些有需要CPU处理的scoket
的事件来占用CPU资源,因此就有了基于事件的模型。事件可以一个计算请求和查询数据库操作等。
我们可以定义一些对应事件的处理方法,当有事件时,然对应的线程执行这些处理方法来处理对应的事件即可。这样做可以充分利用CPU资源。
Reactor和Proactor模型都是基于事件的模型
一个客户端的请求,分为两步:
- 发送数据,服务器接收数据
- 接收数据解析后,执行对应的请求
Reactor和Proactor的核心区别就在接收数据的这个过程上。
Reactor
Reactor模式的核心是:获取一个或多个用户在某个时间的请求,把这些请求时间同步且顺序的交给对应的线程处理(也可能是获取请求的线程),之后处理线程读取用户数据,然后解析数据并进行有关的操作。
这里,一个请求的到来,就是一个事件,之后这个事件会同步地激发对应的处理函数,并且把函数交给对应的线程区处理。select、poll、epoll和kqueue都是这种模式。
可以参考这两篇文章:
- https://blog.csdn.net/qq_35976351/article/details/85130403
- https://blog.csdn.net/qq_35976351/article/details/85228002
以并发的epoll模式为例子,假设epoll本身在主线程中,epoll获取由IO通知事件的线程是处理线程,那么主线程获取一个批次的客户端请求之后,会依次把对应的文件描述符传递给处理线程组,处理线程组负责完成数据IO和之后的处理操作。文件描数符就可以视为socket,处理线程组通过和socket交互,完成一次事件的处理。
该批次的文件描数符是同步的发送给各个工作线程的;而且工作线程是非阻塞的同步IO机制,因为接收到文件描数符的时候,就已经是由IO数据了。
优势
- 整个事件循环是可见的,比如epoll模式,我们手动写的事件循环
- 初步模块化,主线程负责接收消息并投递,工作线程负责IO和之后的逻辑处理,并且可以模块化的设计各个方法
- 可以控制优先级,因为投递方式对用户是可见的
劣势
- 工作线程需要主动IO,这使CPU没法充分使用;如果是多级的任务,比如处理完成请求后回发数据等,会进一步浪费CPU时间
- 需要手动管理线程和一些同步操作,可扩展性不强,只能靠封装绑定不同的方法来投递给处理线程
- 如果一次请求涉及到多次数据传输,则会大大增加代码的复杂度;比如工作线程获取数据后,又需要多次请求其它不同的服务器获取有关的数据,那么需要在工作线程内部再次进行IO和网络操作。
- 不容易调试和测试
Proactor
Proactor模式的核心是:操作系统维护一个事件循环,每个IO操作都绑定对应的回调函数,IO完成才是一个事件,IO事件会触发对应的回调函数,回调函数由对应的工作线程执行。
典型就是各类异步回调模式,比如boost的asio库。这种模式,需要先启动一个事件循环,然后把对应的IO操作绑定对应的回调函数,当IO完成之后,工作线程就会执行对应的回调函数了。
优势
- IO和工作分离,可利用对应CPU时间;IO由操作系统或者对应的事件循环去做了,工作线程只处理IO完成的任务即可
- 并发策略和线程管理策略分离,简化程序
- 简化线程同步操作
劣势
- 事件循环不可见,无法调整调度的优先级
- 异步的效率可能与平台有关
- debug和test会更复杂