Reactor模式

反应器设计模式(Reator pattern)是一种基于事件驱动的设计模式,常用于高并发场景下,常见的像Node.js、Netty、Vert.x中都有着Reactor模式的身影。本文对Reactor模式作了简要介绍,结合对比Node.js线程模型进行分析。

单线程Reactor模式

Reactor模式是一种事件处理模式,单个或多个事件并发地投递到事件处理服务,事件处理服务将事件进行分离,同步的将他们分发到对应的事件处理器中。

模式结构

下图是一张示意图,结合了维基百科的定义:

Reactor模式_第1张图片
  • Handle:句柄,是对资源在操作系统层面的抽象(unix中的fd),可供系统输入或输出的资源。在网络编程中,一般指的是一个连接,如socket,Java NIO中的Channel
  • Demultiplexer:同步事件分离器,通常使用EventLoop来进行资源的阻塞等待,当一个资源处于就绪状态的时候,会被轮询出来传递给分发器
  • Dispatcher:分发器,用来注册、移除EventHandler,将资源分配到对应的处理器中同步执行
  • EventHandler:事件处理器,处理对应的事件,一般由分发器进行回调。

交互过程

  • 初始化Dispatcher
  • 注册EventHandler到Dispatcher,每个事件处理器包含对应Handle的引用,这样就可以建立Handle到EventHandler的映射
  • 启动EventLoop,阻塞等待资源的某个事件发生
  • 当某些Handle的事件发生后,资源变为就绪状态,会被传递给Dispatcher,分发器通过开始注册的资源映射关系,调用对应的EventHandler方法。

这就是最简单的Reactor模式,它其实也是I/O多路复用的一种具现,用户不需要考虑并发的问题,直接交给了事件处理器进行,同时也减少了高并发情况下多线程对系统资源的消耗。

在一些小容量的场景下,单线程的模式可以使用,但在高负载、大并发的场景下却不适用,主要在于一个NIO线程无法支撑同时处理成百上千的链路处理,在编解码消息时,由于无法及时处理会造成消息堆积,进而形成请求超时等问题,而且单个线程也不利于程序的稳定性,一旦线程崩了,整个程序就崩掉了。所以在实际应用中大部分都是多线程的Reactor模式。

Node.js线程模型

Node.js是一个采用事件驱动和异步非阻塞I/O,实现的单线程、高并发的JavaScript运行环境,使用它可以编写性能良好的web服务端。笔者在学习Vert.x的时候发现它与Node.js的线程模型很类似,它并不是严格意义上的Reactor模式,这里用来与单线程的Reactor模式作对比,加深理解。

众所周知,I/O操作绝大部分都十分耗时,传统的方法是使用多线程来解决I/O耗时阻塞的问题,多路复用也可以实现同时处理多个任务的功能,那么单线程的IO是如何处理I/O的并发请求呢?

Node.js并不是单纯的单线程,它用主线程处理所有请求,然后对I/O操作进行异步处理,交给其他线程去执行,避免了频繁创建、销毁和上下文切换带来的系统开销。下面来看Node.js的工作原理。

工作原理

Reactor模式_第2张图片

从左到右,从上到下,Node.js 被分为了四层,分别是 应用层、V8引擎层、Node API层 和 LIBUV层。

  • 应用层: 即 JavaScript 交互层,常见的就是 Node.js 的模块,比如 http,fs
  • V8引擎层: 即利用 V8 引擎来解析JavaScript 语法,进而和下层 API 交互
  • NodeAPI层: 为上层模块提供系统调用,一般是由 C 语言来实现,和操作系统进行交互
  • LIBUV层: 是跨平台的底层封装,实现了 事件循环、文件操作等,是 Node.js 实现异步的核心

Node.js在主线程维护了一个事件队列,接收到请求后,就将该请求作为一个事件放入Event Queue中,然后继续接受其他请求,当主线程空闲(没有请求接收) 的时候,就开始轮询事件队列。这里要分两种情况:

  • 普通任务,就由主线程亲自执行,并通过回调函数返回给上层调用
  • I/O任务,就从线程池中拿出一个线程处理这个事件,指定回调函数,继续轮询事件队列中的其他事件。当线程中的I/O任务完成以后,执行回调函数,并把这个完成的事件放在事件队列的尾部,等待事件循环,当主线程再次循环到该完成事件时,再返回给上层调用。

Node.js的单线程并非整个环境都运行在单线程中,而是对JavaScript层面的任务处理是单线程的。

像Node.js这种解决方案:将耗时少的短任务交给主线程来处理,将I/O操作或者一些CPU密集型任务交给其他线程来执行,这样不会阻塞EventLoop正常进行事件的循环,是比较通用的解决方案。例如在Vert.x中,对于普通的verticle,会运行在EventLoop线程中,对于耗时长的任务则放在Worker Pool中的线程上运行。

多线程下的Reactor模式与Node.js这种解决方案很类似,一个线程负责链路的监听、建立,然后将I/O交给其它子线程来完成。后文会有介绍。

你可能感兴趣的:(Reactor模式)