单线程OR多线程

一些理解

  • 我们写出来的js代码,是在单线程的环境中执行,但nodejs本身不是单线程的。如果我们在代码中执行了一个promise,它们可能是通过底层的c++模块在另外的线程中完成?… 但对于我们自己的js代码来说,它们处于单线程中。因为异步函数执行完将结果通过回调函数传给我们的时候,我们的代码一次只能处理一个。

  • node擅长处理高并发,是因为拥有异步IO,node在处理IO时主线程会创建N个子线程以提高速度,虽然开启了多个线程,但是所有线程都是基于主线程开启的只能跑在一个进程当中并不能充分利用cpu资源

单线程的问题

1
2
3
4
5
6
7
8
9
var start = Date.now();//获取当前时间戳
setTimeout(function () {
    console.log(Date.now() - start);
    for (var i = 0; i < 10000000000; i++){//执行长循环
    }
}, 1000);
setTimeout(function () {
    console.log(Date.now() - start);
}, 2000);

上面执行后的输出结果为1000、13322,足足等了10秒钟,console.log才被执行,这个for循环是一个非常吃CPU的计算过程,而Node的主线程却只能调用一个CPU来计算它(线程是cpu调度的一个基本单位,一个cpu同时只能执行一个线程的任务),但是如果我们在写JS代码时能像Node底层开启另外一个线程处理异步的方式那样,手动创建子线程,这个问题就解决了。

事件驱动和异步I/O

所以nodejs只有一个主线程(single thread)运行,通过一个事件循环(event-loop)来循环取出消息队列(event-queue)中的消息进行处理,处理过程基本上就是去调用该消息对应的回调函数。消息队列就是当一个事件状态发生变化时,就将一个消息压入队列中。

nodejs的时间驱动模型一般要注意下面几个点:

因为是单线程的,所以当顺序执行js文件中的代码的时候,事件循环是被暂停的。

当js文件执行完以后,事件循环开始运行,并从消息队列中取出消息,开始执行回调函数

因为是单线程的,所以当回调函数被执行的时候,事件循环是被暂停的

当涉及到I/O操作的时候,nodejs会开一个独立的线程来进行异步I/O操作,操作结束以后将消息压入消息队列。

如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
var fs = require("fs");
var debug = require('debug')('example1');

debug("begin");

fs.readFile('package.json','utf-8',function(err,data){
    if(err)  
        debug(err);
    else
        debug("get file content");
});

setTimeout(function(){
    debug("timeout2");
});

debug('end'); // 运行到这里之前,事件循环是暂停的

上述代码执行顺序: 1. 同步执行debug(“begin”)

  1. 异步调用fs.readFile(),此时会开一个新的线程去进行异步I/O操作

  2. 异步调用setTimeout(),马上将超时信息压入到消息队列中

  3. 同步调用debug(“end”)

  4. 开启事件循环,弹出消息队列中的信息(目前是超时信息)

  5. 然后执行信息对应的回调函数(事件循环又被暂停)

  6. 回调函数执行结束后,开始事件循环(目前消息队列中没有任何东西,文件还没读完)

  7. 异步I/O读取文件完毕,将消息压入消息队列(消息中含有文件内容或者是出错信息)

  8. 事件循环取得消息,执行回调

  9. 程序退出。

盗来的图~~~

事件循环的工作过程

底层多个线程处理事件的原理

最后:

使用单线程的Node写代码时应该注意的东西

  • 上面的测试说明了,在主线程做一些CPU计算量很大的任务可能会导致主线程卡住或者长时间停顿从而让整个程序的执行延时甚至卡死,影响程序性能,所以我们要非常小心的处理大量的循环,字符串拼接和浮点运算这种cpu密集型任务,在需要的时候应该利用上面提到的cluster和多线程模型把任务丢给子线程或子进程去完成,保持程序主线程的畅通。