首页 > 代码库 > 关于事件模型的一些看法

关于事件模型的一些看法

http://forkme.info/about-event-loop/

事件处理模型, 也即是全异步事件处理模型。在以前, 对于那些同时执行多项任务, 但仍能响应用户交互的应用程序通常需要实施一种使用多进程(如linux的fork操作)或者多线程的操作。对于低并发的环境, 这样做无疑能避免进程因等待某个操作而出现"假死"现象。但对于更复杂的异步应用程序或者是要求高并发的环境, 就要使用事件模型来处理异步事件, 这样做有很多好处:

  • 在高并发条件下响应用户时间更快;
  • 内存消耗降低, 能处理更多的用户请求;
  • 单进程, 在高并发下避免因为线程或进程之间切换带来的时间片消耗;
  • 有效降低死锁发生的频率

当然, 基于事件的处理模型也有其缺点:

  • 事件处理器在处理的时候要尽量的快, 否则就会阻塞主线程;
  • 进程会开辟额外的内存空间来维护请求状态;
  • 事件模型一般是单进程操作, 不能有效利用多核cpu的性能(可以通过创建子线程解决问题)

当前, 基于事件的处理模型被应用到了各种环境之中。譬如linux的epoll模块, javascript语言以及Nginx服务器等。

以下面的javascript代码作为例子来讲解事件处理模型的特点:

  1. <script type="text/javascript">
  2. var callback = function () {
  3. alert(‘Hello world‘);
  4. };
  5. doucment.onclick = callback;
  6. ......
  7. </script>

这就是实现事件处理模型的一般模式: 首先定义一个或多个异步事件, 每一个事件绑定回调函数。主进程注册完事件后就继续执行, 只有当事件被触发时才会执行回调函数。放到例子当中就是当用户点击文档时会弹出窗口并显示 Hello world。

综上: 基于事件模型的程序要具备以下结构, 如图:

Git Bash

    1. 事件源: 就是事件的来源或者事件的产生者, 程序需要对事件源进行操作;
    1. 事件分离器: 获取事件池中的事件并根据事件源找到不同的事件处理器按顺序进行处理;
    1. 事件处理器: 即是上文说到的回调函数, 用于处理具体业务;
    1. 事件循环: 周期性的获取事件源中的事件进行分发

Event Loop是一个很重要的概念, 指的是计算机系统实现的一种运行方式, 主要用于等待并发送消息和事件。虽然在每一种实现方式中具体的实现方法可能不同(例如Nginx事件模型和javascript事件模型), 但是都需要实现并维护Event Loop。 在计算机中, 每一个运行的程序就叫做进程(process)。 一般情况下, 一个进程只能一次只能执行一个任务。如果想要执行多个任务, 不外乎以下三种解决方式:

  • 1 排队: 每次进程只能执行一个任务, 只好等前面的任务执行完了, 再执行后面的任务;
  • 2 新建进程: 对于每一次请求fork新的进程进行处理;
  • 3 新建线程: 对于每一个请求新建线程进行处理

以上2和3方式分别对应多进程和多线程模型, 优劣在之前已经讲述。而事件处理模型正是基于上面第一种方式, 这样做可以避免不必要的进程或线程间切换, 保证进程的高效运行。但是有一个问题, 一旦遇到大量任务或者是遇到一个耗时的操作, 进程就会被阻塞, 出现所谓的"假死"现象, 无法响应其他操作。

按照上面的运行模型, 如果某个任务很耗时,那么进程的运行大概是这个样子:

Git Bash

上图的绿色部分是程序运行时间, 红色部分是等待时间。这个进程在大部分时间都在等待其他操作, 这种运行方式称为"同步模式"(synchronous I/O)或"堵塞模式"(blocking I/O)。

如果采用多进程或者多线程, 很可能就是下面这种情况:

Git Bash

上图表明, 比起单进程, 多进程耗费成倍的系统资源并闲置等待, 这显然不合理。

Event Loop就是为了解决单进程阻塞问题而提出来的一种合理方案。以javascript作为例子, 在程序中设置一个主进程和若干个子线程(以1个子线程为例)。主进程负责程序本身的运行并处理"非阻塞"操作; 子线程负责与其他进程(主要是I/O操作等耗时进程)的通信, 这个线程就被称为"Event Loop线程"。

Git Bash

上图橙色表示空闲时间。每当遇到I/O操作等耗时操作时, 主进程就让Event Loop线程去通知相应的I/O程序并注册相应的事件和设置回调函数, 然后主线程接着往后运行, 所以不出现红色的阻塞时间, 等到I/O操作完成, Event Loop线程再把结果返回主进程, 主进程调用实现设定的回调函数完成任务(注意: 回调函数不能执行太多的耗时操作, 因为此时主进程是处于阻塞状态的)。这种运行方式称为"异步模式"(asynchronous I/O)或"非堵塞模式"(non-blocking mode)。这也是事件处理模型的内部实现机制。

(1)上下文含义

在计算机中, 上下文代表着很多种含义。在基于事件的模型中,上下文表示什么?简单的讲, 以javascript为例, 就是在主进程注册事件后将回调函数作用域内的变量保存成一个对象保存起来, 这个对象就叫做上下文对象。每一个事件的回调函数都包含着自己的上下文对象, 当对应事件被调用并执行回调函数时, 函数可以直接通过上下文对象获得作用域内保存变量的值, 这个操作对开发者是透明的, 开发着只需要直接写对象名称就可以了。

(2)为什么要定义上下文

在基于事件的模型中, 上下文是必须存在的, 为什么要定义上下文这个概念呢?因为基于事件的模型都是基于异步机制的。以javascript为例, 每次主进程执行到结尾的时候就会释放其作用域内的所有变量。换句话说, 不管事件有没有被触发, 主进程执行完所有的逻辑之后就会销毁所有的变量。如果没有上下文的话, 当用户触发事件执行回调函数时, 就会无法找到其作用域内的变量, 使用上下文就是为了让每一个特定的回调函数能访问到属于自己作用域内的变量的值。

在实现一个基于事件模型的应用的过程中, 有一个问题是必须考虑到的, 那就是主进程究竟要创建多少子线程才比较合理, 也就是说Event Loop什么时候需要创建。如果创建的子线程太多或是太少, 会出现以下问题:

  • 1 线程创建太多: 线程越多, 线程间的切换就越频繁, cpu就会消耗更多的时间在线程间的切换上。导致实际处理逻辑执行时间过短;
  • 2 线程创建太少: 线程越少, 就会有越多的阻塞操作集中在同一个线程中, 会影响同一线程内其他阻塞操作的执行

子线程的数目主要要根据应用处理的业务类型, 具体机器的内存和cpu等因素决定, 并没有一致的规定。以下做法是比较好的一种实践:

  • 对于大文件上传, 下载等耗时比较长的操作建议是每次创建新的线程来处理;
  • 根据业务类型, 用户比较急需的阻塞操作可以独开线程处理;
  • 对于耗时较短而且是不固定时间的操作, 比如点击事件等, 可以用一个Event Loop来放置这些阻塞事件

对于高并发, 多请求的事件, 传统的多线程和多进程的方法已经难以应付这些情况。而基于事件的模型由于采用的是异步的方式, 通过类似于中断上下文的操作, 能获得很好的并发性和高性能。并且能避免之前模型出现的一些棘手问题, 比如死锁问题。随着应用复杂度飞不断提高, 相信会有更多的应用将会采用基于事件的模型来应对这些情况。

关于事件模型的一些看法