作为一个高性能服务器程序通常需要考虑处理三类事件: I/O 事件,定时事件及信号。两种高效的事件处理模型:Reactor 和 Proactor。
-
1、reactor模式
主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话立即通知工作线程(逻辑单元 ),读写数据、接受新连接及处理客户请求均在工作线程中完成。通常由同步I/O实现。
Reactor是同步IO,同步IO需要线程自己等待内核准备好数据,在内核准备数据的过程中,当前线程是阻塞的,这样就会导致如果某个线程因为读取IO的时间过长(比如读取文件、写文件),则它势必会影响其他线程的执行。
Reactor通过select监控连接事件,收到事件后通过dispatch进行分发,如果是连接建立的事件,则交给Acceptor处理,否则会调用Handler进行处理。优点:没有进程间通信,进程竞争,都在一个进程内完成。缺点:只有一个进程,无法发挥多核CPU优势,handler在处理业务时,整个进程无法处理其他连接的事件,性能瓶颈严重。
Redis采用这种方法,因为每个操作都非常快结束
Reactor监听连接建立事件,handler只负责读写操作,业务处理交给子线程processor完成,process完成后将结果交给handler,handler写入结果。优点:能发挥多核CPU的处理能力。缺点:多线程数据共享和访问比较复杂,Reactor监听所有事件,并且只在主线程中运行,瞬间高并发时会成为性能瓶颈
主Reactor只处理连接事件的建立,然后把连接交给子进程Reactor进行监听,子进程创建handler进行完整业务处理。优点:主Reactor和子Reactor职责明确,交互简单,子进程之间不需要共享同步之类的处理。
Nginx采用的就是多Reactor多进程,Memcache和Netty用的是多Reactor多线程
-
2、 proactor模式
主线程和内核负责处理读写数据、接受新连接等I/O操作,工作线程仅负责业务逻辑,如处理客户请求。通常由异步I/O实现。
-
3、以读操作为例来看看Reactor中的具体步骤:
读取操作:
应用程序注册读就需事件和相关联的事件处理器
事件分离器等待事件的发生
当发生读就需事件的时候,事件分离器调用第一步注册的事件处理器
事件处理器首先执行实际的读取操作,然后根据读取到的内容进行进一步的处理 -
4、Proactor模式中读取操作和写入操作的过程:
读取操作:
应用程序初始化一个异步读取操作,然后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键。
事件分离器等待读取操作完成事件
在事件分离器等待读取操作完成的时候,操作系统调用内核线程完成读取操作,并将读取的内容放入用户传递过来的缓存区中。这也是区别于Reactor的一点,Proactor中,应用程序需要传递缓存区。
事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区读取数据,而不需要进行实际的读取操作。
Proactor中写入操作和读取操作,只不过感兴趣的事件是写入完成事件。
从上面可以看出,Reactor和Proactor模式的主要区别就是真正的读取和写入操作是有谁来完成的,Reactor中需要应用程序自己读取或者写入数据,而Proactor模式中,应用程序不需要进行实际的读写过程,它只需要从缓存区读取或者写入即可,操作系统会读取缓存区或者写入缓存区到真正的IO设备.
综上所述,同步和异步是相对于应用和内核的交互方式而言的,同步 需要主动去询问,而异步的时候内核在IO事件发生的时候通知应用程序,而阻塞和非阻塞仅仅是系统在调用系统调用的时候函数的实现方式而已。