nodejs事件轮询详述

目录html

 

概述node

关于nodejs的介绍网上资料很是多,最近因为在整理一些函数式编程的资料时,屡次遇到nodejs有关的内容。因此就打算专门写一篇文章总结一下nodejs相关知识,包括“说它单线程是什么意思”、“非阻塞又是指什么”以及最重要的是它的“事件轮询”的实现机制。编程

本文不介绍nodejs的优缺点(适用场合)、nodejs环境怎样搭建以及一些nodejs库的使用等等这些基础知识。多线程

 

nodejs特色异步

网上任何一篇关于nodejs的介绍中均会说起到nodejs两个主要特色:单线程、非阻塞。可是据我所了解到的,大部分介绍一带而过,并无详细地、系统性地去说明它们究竟是怎么回事。下面我依次尽我所能详细地说一下我对以上二者的理解。函数式编程

非阻塞异步编程

咱们先来看一段.NET中异步编程的代码:函数

using(FileStream fs = new FileStream("hello.txt", FileMode.Open))
{
    byte[] data = new byte[fs.Length];
    fs.BeginRead(data, 0, fs.Length, new AsyncCallback(onRead), null);
    Console.WriteLine("the end");
}

如上代码所示,因为FileStream.BeginRead是一个异步方法,因此无论hello.txt文件有多大,FileStream.BeginRead方法的调用并不会阻塞调用线程,Console.WriteLine方法立马即可执行。同理,若是在nodejs中全部的方法都是“异步方法”,那么在nodejs中任何方法的调用均不会阻塞调用线程,实质上,nodejs中大部分库方法确实是这样的。这就是为何咱们会说nodejs中代码是非阻塞的。网站

单线程ui

对这个概念有误解的人很是之多,觉得nodejs程序中就一个线程,而后有不少人会问:既然只有一个线程,那么怎么并行处理多个任务呢?

其实这里说的单线程并非指nodejs程序中只有一个线程存在,我我的感受官方给出“单线程”说法自己就具备误导性,因此也怪不得大部分初学者。那么“单线程”到底什么意思呢?其实这里的“单线程”指的是咱们(开发者)编写的代码只能运行在一个线程当中(能够称之为主线程吧),就像咱们在Windows桌面程序开发中同样,编写的全部界面代码均运行在UI线程之中。

那么仍是刚才那个问题,全部编写的代码均运行在一个线程中,那么怎样去并行处理任务呢?这个就要想到前面介绍的“异步方法”了,没错,虽然开发者编写的全部代码均运行在一个线程中,可是咱们能够在这个线程中调用异步方法啊,而异步方法内部实现过程固然要采用多线程了。就像下图:

如上图所示,nodejs中的单线程指的是图中的主线程,该主线程中包含一个循环结构,维持整个程序持续运转。

注:该循环结构也称之为“泵”结构,是每一个系统必备的结构。具体能够参见我以前的一篇博客《动力之源:代码中的泵》

所以咱们能够说,在nodejs中写的代码(包括回调方法)均只运行在一个线程中,可是不表明它只有一个线程。nodejs中许多异步方法在具体的实现时,内部均采用了多线程机制(具体后面会讲到)。

 

事件轮询

若是看过我前面博客的一些读者可能知道,一个系统(或者说一个程序)中必须至少包含一个大的循环结构(我称之为“泵”),它是维持系统持续运行的前提。nodejs中同样包含这样的结构,咱们叫它“事件轮询”,它存在于主线程中,负责不停地调用开发者编写的代码。咱们能够查看nodejs官方网站上对nodejs的说明:

咱们能够看到,在nodejs中这个“循环”结构对开发者来说是不可见的。

那么开发者编写的代码是怎样经过事件轮询来获得调用的呢?尤为是一些异步方法中带的回调函数?看下面一张图:

如上图所示,每一个异步函数执行结束后,都会在事件队列中追加一个事件(同时保存一些必要参数)。事件轮询下一次循环即可取出事件,而后会调用异步方法对应的回调函数(参数)。这样一来,nodejs便能保证开发者编写的每行代码(每一个回调)均在主线程中执行。注意这里有一个问题,若是开发者在回调函数中调用了阻塞方法,那么整个事件轮询就会阻塞,事件队列中的事件得不到及时处理。正由于这样,nodejs中的一些库方法均是异步的,也提倡用户调用异步方法。

其实看到这里的时候,若是有对Windows编程(尤为对Windows界面编程)比较了解的读者可能已经联想到了Windows消息循环。

没错,nodejs中的事件轮询原理跟Windows消息循环的原理相似。开发者编写的代码均运行在主线程中,若是你编写了阻塞代码,在Windows桌面程序中,因为消息得不到及时处理,界面就会卡死。

我们再来看一下下面的nodejs代码:

var fs = require('fs');
fs.readFile('hello.txt', function (err, data) {  //异步读取文件
  console.log("read file end");
});
while(1)
{
    console.log("call readFile over");
}

如上,虽然咱们使用异步方法读取文件,可是文件读取完毕后“read file end”永远不会输出,也就是说readFile方法的回调函数不会执行。缘由很简单,由于后面的while循环一直没退出,致使下一次事件轮询不能开始,因此回调函数不能执行(包括其余全部回调)。事实再次证实,开发者编写的全部代码均只能运行在同一线程之中(姑且称之为主线程吧)。

 

关于异步方法

所谓异步方法,就是调用该方法不会阻塞调用线程,哪怕方法内部要进行耗时操做。你能够理解为方法内部单独开辟了一个新线程去处理任务,而调用异步方法仅仅是开启这个新线程。下面的代码模拟一个异步方法的内部结构(仅仅是模拟,不表明实际):

public void DoSomething(int arg1,AsyncCallback callback)
{
    (Action)(delegate()
    {
         Thread.Sleep(1000*20);  //模拟耗时操做
         if(callback != null)
         {
              callback(...);  //调用回调函数
         }
    }).BeginInvoke(null,null);

}

如上代码所示,调用DoSomething方法不会阻塞调用线程。那么对于每个异步方法,怎样去判断异步操做是否执行完毕呢?这时候必须给异步方法传递一个回调函数做为参数,在.NET中,这个回调参数通常是AsyncCallback类型的。如你们所熟知的FileStream.BeginRead/BeginWrite以及Socket.BeginReceive/BeginSend等等均属于该类方法。

可是,我之因此要提异步方法,就是想让你们区分nodejs中的异步方法和.NET中异步方法的一个重大区别,虽然二者内部原理能够理解为一致的,可是在回调函数的调用方式这一点上,二者有大相径庭的方式。

在.NET中,每一个异步方法的回调函数均在另一个线程中执行(非调用线程),而在nodejs中,每一个异步方法的回调函数仍然还在调用线程上执行。至于为何,你们能够看一下前面讲事件轮询的部分,nodejs中每一个回调函数均由主线程中的事件轮询来调用。这样才能保证在nodejs中,开发者编写的任何代码均在同一个线程中运行(所谓的单线程)。

注:不懂调用线程、当前线程是什么意思的同窗能够看一下这篇博客:《高屋建瓴:梳理编程约定》

相关文章
相关标签/搜索