1. Reactor和Preactor模式
- Reactor用于同步IO,Preactor用于异步IO
- Reactor通常会和Connector模式一起使用,进一步解耦连接的建立与连接以后的逻辑
2. Reactor模式中的主要角色
2.1. Reactor
Reactor是IO事件的派发者。
2.2. Acceptor
Acceptor接受client连接,建立对应client的Handler,并向Reactor注册此Handler。
2.3. Handler
和一个client通讯的实体,按这样的过程实现业务的处理。一般在基本的Handler基础上还会有更进一步的层次划分, 用来抽象诸如decode,process和encoder这些过程。比如对Web Server而言,decode通常是HTTP请求的解析, process的过程会进一步涉及到Listner和Servlet的调用。业务逻辑的处理在Reactor模式里被分散的IO事件所打破, 所以Handler需要有适当的机制在所需的信息还不全(读到一半)的时候保存上下文,并在下一次IO事件到来的 时候(另一半可读了)能继续中断的处理。为了简化设计,Handler通常被设计成状态机,按GoF的state
pattern来 实现。
3. 多线程下的Reactor
一个突然闪入脑海的问题:在单核的机器上,如果除了Reactor可能阻塞在等待IO事件中以外,Handler和Acceptor都是完全 非阻塞的,那用多线程来实现Reactor模式能获得性能提升吗?(貌似不能……)
3.1. 多线程化的目的
- 利用上多核的计算能力。
- 策略性地增减线程从而提升伸缩性。
- 要让Reactor能快速派发IO事件到Handler中。如果用单线程,Handler处理快慢直接影响队列中后续IO事件的派发速度。
- IO之外的处理交给其他线程,这样Reactor可以专心派发事件。
- 可以采用多Reactor的设计,因为:
- 如果负载高的话,单个Reactor可能会忙于IO,影响伸缩性。
- 其实多Reactor就是一种负载均衡的策略,从而不会让CPU在IO十分频繁的时候空闲下来。
- 线程划分为Reactor线程和Worker线程,前者处理IO并派发事件,后者来跑Handler做完IO后的Process处理。
Doug Lea的《Scalable IO in Java》中实例代码中,多线程版本的Handler还是在Reactor线程里运行,只有在读到 需要的数据了以后才会想Worker线程池里提交process的任务。个人对于这种有一个问题:如果不能一次读到全部需要 的数据,process过程该如何设计? (TODO:争取从Jetty中找到一些答案)
3.1.1. Worker线程
- 分担非IO的业务逻辑处理,减轻Reactor负担
- 用线程池来管理,以便做性能和伸缩性之间的微调和控制
- 通常需要的线程远远低于同时连接的client数量
- 任务之间的协调(TODO:需要更进一步的了解)
- Handoff
- Callback
- Queue
- Future
4. Java中的实现
Java中Reactor模式server的实现主要应用的两大块技术就是:
- Java5引入的concurrent包(传送门)
- ThreadPoolExecutor实现线程池
- Queue, Future等等机制的使用
- Java NIO(传送门)
- Channel
- Buffer
- Selector
4.1. 多Reactor线程的实现
- 静态创建 vs 动态创建
- 每个Reactor都有独立的Selector和对应的线程
- Reactor划分为main reactor和sub reactor
- main reactor监听连接请求并控制acceptor(单个main reactor?)
- sub reactors监听连接并分发IO事件到handler里
- Acceptor用某种机制来将accept后创建的handler分发到Reactor中
- Round-robin?
- 负载均衡?
4.2. 应用NIO其他Feature
- 单Reactor多Selector的模式:将不同的handlers绑定到不同的IO事件上
- 需要控制好同步
- 文件传送:应用OS提供的sendfile调用来直接将文件数据发送到网络连接中,省去了用户空间和内核空间的一次数据拷贝
- Direct Buffers:可以实现零拷贝的数据传送,但有创建和销毁的开销,适合长连接应用
- 参见NIO相关话题(传送门)
下一个介绍:
Reactor和preactor都是IO多路复用模式,一般地,I/O多路复用机制都依赖于一个事件多路分离器(Event Demultiplexer)。分离器对象可将来自事件源的I/O事件分离出来,并分发到对应的read/write事件处理器(Event Handler)。开发人员预先注册需要处理的事件及其事件处理器(或回调函数)。
Reactor模式采用同步IO,而Proactor采用异步IO。同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪,而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知(异步的特点就是通知)。
而阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会立即返回一个状态值。
同步阻塞: 在此种方式下,用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。 |
同步非阻塞: 在此种方式下,用户进程发起一个IO操作以后边可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。 |
异步阻塞: 此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,那么为什么说是阻塞的呢?因为此时(通知)是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄(就绪的没有就绪的都有监听,epoll是select的替代方式,只监听就绪的文件句柄),从而提高系统的并发性! |
异步非阻塞: 在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。 |
reactor |
proactor |
1. 应用程序注册读/写就绪事件和相关联的事件处理器 2. 事件分离器等待事件的发生 (Reactor负责) 3. 当发生读就绪事件的时候,事件分离器调用第一步注册的事件处理器(Reactor负责) 4. 事件处理器首先执行实际的读取操作,然后根据读取到的内容进行进一步的处理(用户处理器负责) |
1. 应用程序初始化一个异步读取操作,然后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键。 2. 事件分离器等待读取操作完成事件 3. 在事件分离器等待读取操作完成的时候,操作系统调用内核线程完成读取操作(异步IO都是操作系统负责将数据读写到应用传递进来的缓冲区供应用程序操作,操作系统扮演了重要角色),并将读取的内容放入用户传递过来的缓存区中。这也是区别于Reactor的一点,Proactor中,应用程序需要传递缓存区。 4. 事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区读取数据,而不需要进行实际的读取操作。 |
与reactor相比,proactor显然系统调用更少。
从上面可以看出,Reactor和Proactor模式的主要区别就是真正的读取和写入操作是有谁来完成的,Reactor中需要应用程序自己读取或者写入数据,而Proactor模式中,应用程序不需要进行实际的读写过程,它只需要从缓存区读取或者写入即可,操作系统会读取缓存区或者写入缓存区到真正的IO设备.
综上所述,同步和异步是相对于应用和内核的交互方式而言的,同步需要主动去询问,而异步的时候内核在IO事件发生的时候通知应用程序,而阻塞和非阻塞仅仅是系统在调用系统调用的时候函数的实现方式而已。
IO多路复用和Reactor模式