NodeJs 事件驱动

1 在了解nodejs的事件驱动模型的时候,先来看下传统线程网络模型

1.1 传统线程网络模型

发起请求,请求进入web服务器(IIS、Apache)之后,会在线程池中分配一个线程来线性同步完成请求处理,直到请求处理完成并发出响应,结束之后线程池回收。

这就会就会带来以下几个问题 :

  • 由于线程池中线程个数有限,对于频繁请求时,就会出现等待,严重的甚至会把服务器挂掉
  • 同时线程的增多也会占用大量的 CPU 时间来处理内存上下文切换, 而且还容易遭受低速连接攻击
  • 对于高并发的时候,为了防止出现脏数据就会使用锁来解决,一些I/O事务可能消耗很长得时间,这样就会出现一些线程等待,效率低下

1.2 Node.Js使用事件驱动模型

当web server接收到请求,就把它关闭然后进行处理,然后去服务下一个web请求。当这个请求完成,它被放回处理队列,当到达队列开头,这个结果被返回给用户。这个模型非常高效可扩展性非常强,因为webserver一直接受请求而不等待任何读写操作。(这也被称之为非阻塞式IO或者事件驱动IO)

Node.js 的异步机制是基于事件的,所有的 磁盘 I/O 、 网络通信、 数据库查询 客户端的请求 都以非阻塞的方式请求,返回的结果由事件循环来处理。如图 描述了这个机制。Node.js 进程在同一时刻只会处理一个事件,完成后立即进入事件循环检查并处理后面的事件。
这样做的好处是CPU 和内存在同一时间集中处理一件事,同时尽可能让耗时的 I/O 操作并行执行

Nodejs事假驱动机制是通过Nodejs内部通过 单线程高效率地 维护事件队列来实现的,没有多线程的资源占用和频繁的上下文切换

  • JavaPython这个可以具有多线程的语言。多线程同步模式是这样的,将cpu分成几个线程,每个线程同步运行。
  • 而node.js采用单线程异步非阻塞模式,也就是说每一个计算独占cpu,遇到I/O请求不阻塞后面的计算,当I/O完成后,以事件的方式通知,继续执行计算

1.3 同步与异步

同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)

  • 所谓同步,就是在发出一个调用之后,在没有得到结果,该调用就不会返回;得到返回值之后,该调用返回
  • 所谓异步,就是在发出一个调用之后,直接返回该调用,所以没有返回结果;也就是说当一个异步调用发出后,调用者不会立刻得到结果,而是在调用发出后,被调用者通过状态的变化通知调用者,或者通过回调函数处理这个调用

举个通俗的例子:你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下”,然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。

异步操作的时候回调函数的第一个参数通常是上一步传入的错误对象,异步操作不能使用try-catch捕获异常,因为在回调函数运行的时候,上一步的操作早就结束了,错误的栈也已经不存在了,所以只能将错误数据传递给回调函数进行处理

1.4 阻塞与非阻塞

常见I/O阻塞 磁盘读取 、 网络通信、 数据库查询 客户端的请求

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

还是上面的例子,你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了,
当然你也要偶尔过几分钟check一下老板有没有返回结果。在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。

1.5 线程和进程

  • 多线程单进程

多线程的 设计之初就是为了在共享的内存程序空间中,实现并行 处理任务,从而达到充分利用CPU的效果。多线程的缺点就是在于执行的时候上下文的切换开销比较大,使得程序的编写和调用复杂化

  • 单线程多进程

为了避免多线程造成的使用不便的问题,有的语言选择使用单线程保持调用的简单化,采用启动多进程的方式来达到充分利用CPU和提升整体的并行处理能力。它的缺点在于业务逻辑复杂的时候,涉及多个I/O操作的时候,因为业务逻辑不能分布到多个进程之间,事务处理时长要远远大于多线程模式。

2 代码实现,理解上述概念

2.1 阻塞与非阻塞

假如hello.txt

1
hello world

以下程序会非阻塞运行,当我们发起一个文件读取操作的时候,不会阻塞js代码的执行,后续的js代码会继续执行,当文件读取完毕之后,会调用回调函数;

1
2
3
4
5
6
7
8
9
10
11
var fs = require('fs');
fs.readFile('hello.txt',function(err,data){
if(err){
console.log(err.stack);
return;
}
console.log(data.toString());
});
console.log("pro is don");
//pro is don
//hello world

以下程序会阻塞运行,当我们发起一个文件读取操作的时候,会阻塞js代码的执行,当文件读取完毕之后,后续的js代码才开始执行

1
2
3
4
5
6
7
var fs = require("fs");
var data = fs.readFileSync('hello.js');
console.log(data.toString());
console.log("pro is done");
//hello world
//pro is don

2.2 事件循环线程与事件队列

对于遇到I/O操作,node.js不会停止后面的文件的内容的执行,会继续执行后续代码,然后另外一个线程处理I/O,处理完毕之后,将回调函数放入事件队列等待执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var fs = require('fs');
console.log("begin");
setTimeout(function(){
console.log("Timeout1");
},100)
fs.readFile('hello.tet',function(err,data){
if(err){
console.log(err.stack);
return;
}
console.log(data.toString());
});
setTimeout(function(){
console.log("Timeout2");
},0)
//即使设置为0,最少也会有5ms的延迟
console.log("end");

输出如下

1
2
3
4
5
begin
end
timeout2
timeout1
helloworld

需要注意的是 setTimeout和readFile的操作,会将回调函数放入事件队列,回调函数执行的顺序取决于文件读取的速度,以及延时任务的时间的大小比较;

通俗来讲,假如文件读取的用了200ms ,那么执行顺序就是上面的输出结果

假如文件读取用了50ms,那么执行顺序就是

1
2
3
4
5
begin
end
timeout2
helloworld
timeout1

3