10

前言

当我们在接触学习Node.js的时候,估计我们看的最多关于Node.js特性的词是单线程异步无阻塞事件驱动。本文通过这几个特征词汇深入聊聊Node.js的特性。


单线程

我们都知道Node.js的runtime是v8,v8在设计之初是Chrome使用在浏览器对JavaScript语言的解析运行引擎,其最大的特点是单线程,而在Node.js对v8的沿用也是针对这一非常重要的特点。什么是单线程?简单来说就是一个进程中只有一个线程,程序顺序执行,前面执行完成才会执行后面的程序。来看个Node.js对http服务的模型:

Node.js单线程

Node.js的单线程指的是主线程是“单线程”,由主要线程去按照编码顺序一步步执行程序代码,假如遇到同步代码阻塞,主线程被占用,后续的程序代码执行就会被卡住。实践一个测试代码:

var http = require('http');

function sleep(time) {
    var _exit = Date.now() + time * 1000;
    while( Date.now() < _exit ) {}
    return ;
}

var server = http.createServer(function(req, res){
    sleep(10);
    res.end('server sleep 10s');
});

server.listen(8080);

下面为代码块的堆栈图:

堆栈图

JavaScript是解析性语言,代码按照编码顺序一行一行被压进stack里面执行,执行完成后移除然后继续压下一行代码块进去执行。上面代码块的堆栈图,当主线程接受了request后,程序被压进同步执行的sleep执行块(我们假设这里就是程序的业务处理),如果在这10s内有第二个request进来就会被压进stack里面等待10s执行完成后再进一步处理下一个请求,后面的请求都会被挂起等待前面的同步执行完成后再执行,所以这也说明Node.js单线程的执行模型,因为这样的特性,我们的页面不能有耗时很长的同步处理程序阻塞了程序的后续执行,而对于耗时过长的程序应该采用异步执行,这里也就是Node.js的第二个特性,异步。


异步

我们平时都会说Node.js是异步,但是所说的异步具体指的是什么异步?更进一步的说应该是主线程的异步处理函数队列+多线程异步I/O

主线程的异步处理函数队列

首先,所谓的主线程的异步处理函数队列指的是主线程的主要执行空间除了stack以及heap外,还有callback queue(回调函数队列),而callback queue是存放了异步处理的回调函数,在一个执行块里面,当里面的同步代码执行完成后,会从callback queue里面取出回调函数一个个执行,我们最常见的异步处理函数就是setTimeout,一个简单的例子来讲述:


function sleep(time) {
    var _exit = Date.now() + time * 1000;
    while( Date.now() < _exit ) {}
    return ;
}

function main(){
    setTimeout(function(){
        console.log('setTimeout run');
    },0);
    sleep(5);
    console.log('after sleep');
}

main();

/** 执行输出
    after sleep
    setTimeout run
**/

下面是代码块的主线程堆栈执行:

主线程异步执行队列

看上图,主线程将main函数压进stack里面一行行解析执行,首先遇到setTimeout方法,因为setTimeout是一个异步处理函数,这里会把setTimeout(callback,timeout),里面的callback函数移进callback queue里面,同时会把自己从主线程的stack里面移除,继续压进后面的执行代码来解析执行,这里继续压进sleep沉睡5s,接下来执行console,等到这里的同步代码执行完成后这个时候就会从callback queue(FIFO顺序)里面取回调函数一个个执行。(题外话:就算setTimeout里面的timeout设置了是0,都是要等待执行块里面的同步代码执行完成后再去执行callback queue里面的代码)这就是异步里面的其一:主线程异步函数处理队列。

多线程异步I/O

一看标题多线程异步I/O可能会有疑问,不是说Node.js是单线程的吗?其实这里并没有冲突,Node.js每个进程里面只有一个主线程来处理程序,所以,主线程是单线程的。而主线程之外调用的I/O处理是通过一个叫做线程池来管理和分配线程来处理I/O,所以对I/O的处理是多线程。而主线程和I/O线程池则通过上面刚刚讲述的主线程的异步处理函数队列来协作。除了上文所说的timers模块里面的setTimeout函数外,Node.js还对文件系统、网络都实现了异步化调用(题外话:系统底层的I/O异步化都是基于c++的异步框架libuv来实现,然后往上层提供JavaScript调用接口)此处之前理解有误,应该是Node.js只对文件系统以及DNS实现了多线程I/O封装,网络I/O还是采用单线程形式(可参考libuv设计概述原文:Important libuv uses a thread pool to make asynchronous file I/O operations possible, but network I/O is always performed in a single thread, each loop’s thread.感谢bd_bai指正)

异步I/O

而Node.js的高性能也是得益于其将阻塞的I/O异步化,使得不影响主逻辑的执行。


事件驱动

文章至此,先简单总结一下Node.js的两个简单特性,每个Node.js进程只有一个主线程在执行程序代码,在执行的过程中Node.js将阻塞的I/O异步化,并将其回调函数插入callback queue里面,等待同步逻辑执行完成后再通过callback queue里面取出回调函数压进stack里面执行。好了,而事件驱动的作用就是取出回调函数。事件驱动又叫事件循环,是指主线程从主线程的异步处理函数队列里面不停循环的读取事件,驱动了所有的异步回调函数的执行。

事件驱动

至此整个Node.js的异步化逻辑可以不断循环的跑起来了,以上则是我们日常所言的Node.js的三大特性以及其原理。


C小K
117 声望6 粉丝