Skip to content

nodejs事件循环机制 #9

@liang520

Description

@liang520

对于高并发的处理


一般而言,高并发都是多线程模型,服务器为每个客户端发来的请求都分配一个线程来处理,使用同步I/O,系统通过切换线程来弥补同步I/O调动的时间开销。比如 Apache 就是这种策略,由于 I/O 一般都是耗时操作,因此这种策略很难实现高性能,但非常简单,可以实现复杂的交互逻辑。
而事实上,大多数网站的服务器端都不会做太多的计算,它们接收到请求以后,把请求交给其它服务来处理(比如读取数据库),然后等着结果返回,最后再把结果发给客户端。因此,Node.js 针对这一事实采用了单线程模型来处理,它不会为每个接入请求分配一个线程,而是用一个主线程处理所有的请求,然后对 I/O 操作进行异步处理,避开了创建、销毁线程以及在线程间切换所需的开销和复杂性。

事件循环


Node.js 在主线程里维护了一个事件队列,当接到请求后,就将该请求作为一个事件放入这个队列中,然后继续接收其他请求。当主线程空闲时(没有请求接入时),就开始循环事件队列,检查队列中是否有要处理的事件,这时要分两种情况:如果是非 I/O 任务,就亲自处理,并通过回调函数返回到上层调用;如果是 I/O 任务,就从 线程池 中拿出一个线程来处理这个事件,并指定回调函数,然后继续循环队列中的其他事件。

当线程中的 I/O 任务完成以后,就执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,等待事件循环,当主线程再次循环到该事件时,就直接处理并返回给上层调用。 这个过程就叫 事件循环 (Event Loop),其运行原理如下图所示:
image
这个图是整个 Node.js 的运行原理,从左到右,从上到下,Node.js 被分为了四层,分别是 应用层V8引擎层Node API层LIBUV层

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

无论是 Linux 平台还是 Windows 平台,Node.js 内部都是通过 线程池 来完成异步 I/O 操作的,而 LIBUV 针对不同平台的差异性实现了统一调用。因此,Node.js 的单线程仅仅是指 JavaScript 运行在单线程中,而并非 Node.js 是单线程。

工作原理


Node.js 实现异步的核心是事件,也就是说,它把每一个任务都当成 事件 来处理,然后通过 Event Loop 模拟了异步的效果,为了更具体、更清晰的理解和接受这个事实,下面我们用伪代码来描述一下其工作原理 。

  • 【1】定义事件队列
    既然是队列,那就是一个先进先出 (FIFO) 的数据结构,我们用JS数组来描述,如下:
/**
 * 定义事件队列
 * 入队:push()
 * 出队:shift()
 * 空队列:length == 0
 */
globalEventQueue: []

利用数组来模拟队列结构:数组的第一个元素是队列的头部,数组的最后一个元素是队列的尾部,push() 就是在队列尾部插入一个元素,shift() 就是从队列头部弹出一个元素。这样就实现了一个简单的事件队列。

  • 【2】定义接收请求入口
    每一个请求都会被拦截并进入处理函数,如下所示:
/**
 * 接收用户请求
 * 每一个请求都会进入到该函数
 * 传递参数request和response
 */
processHttpRequest:function(request,response){
     
    // 定义一个事件对象
    var event = createEvent({
        params:request.params, // 传递请求参数
        result:null, // 存放请求结果
        callback:function(){} // 指定回调函数
    });
 
    // 在队列的尾部添加该事件  
    globalEventQueue.push(event);
}

这个函数很简单,就是把用户的请求包装成事件,放到队列里,然后继续接收其他请求。

  • 【3】定义 Event Loop
    当主线程处于空闲时就开始循环事件队列,所以我们还要定义一个函数来循环事件队列:
/**
 * 事件循环主体,主线程择机执行
 * 循环遍历事件队列
 * 处理非IO任务
 * 处理IO任务
 * 执行回调,返回给上层
 */
eventLoop:function(){
    // 如果队列不为空,就继续循环
    while(this.globalEventQueue.length > 0){
         
        // 从队列的头部拿出一个事件
        var event = this.globalEventQueue.shift();
         
        // 如果是耗时任务
        if(isIOTask(event)){
            // 从线程池里拿出一个线程
            var thread = getThreadFromThreadPool();
            // 交给线程处理
            thread.handleIOTask(event)
        }else {
            // 非耗时任务处理后,直接返回结果
            var result = handleEvent(event);
            // 最终通过回调函数返回给V8,再由V8返回给应用程序
            event.callback.call(null,result);
        }
    }
}

主线程不停的检测事件队列,对于 I/O 任务,就交给线程池来处理,非 I/O 任务就自己处理并返回。

  • 【4】处理 I/O 任务
    线程池接到任务以后,直接处理IO操作,比如读取数据库:
/**
 * 处理IO任务
 * 完成后将事件添加到队列尾部
 * 释放线程
 */
handleIOTask:function(event){
    //当前线程
    var curThread = this;
 
    // 操作数据库
    var optDatabase = function(params,callback){
        var result = readDataFromDb(params);
        callback.call(null,result)
    };
     
    // 执行IO任务
    optDatabase(event.params,function(result){
        // 返回结果存入事件对象中
        event.result = result;
 
        // IO完成后,将不再是耗时任务
        event.isIOTask = false;
         
        // 将该事件重新添加到队列的尾部
        this.globalEventQueue.push(event);
         
        // 释放当前线程
        releaseThread(curThread)
    })
}

当 I/O 任务完成以后就执行回调,把请求结果存入事件中,并将该事件重新放入队列中,等待循环,最后释放当前线程,当主线程再次循环到该事件时,就直接处理了。

总结以上过程我们发现,Node.js 只用了一个主线程来接收请求,但它接收请求以后并没有直接做处理,而是放到了事件队列中,然后又去接收其他请求了,空闲的时候,再通过 Event Loop 来处理这些事件,从而实现了异步效果,当然对于IO类任务还需要依赖于系统层面的线程池来处理。

因此,我们可以简单的理解为:Node.js 本身是一个多线程平台,而它对 JavaScript 层面的任务处理是单线程的。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions