单线程无阻塞IO模型在Node.js中的工作方式


325

我不是Node程序员,但是我对单线程无阻塞IO模型的工作方式感兴趣。在阅读了理解理解节点事件循环文章之后,我对此感到非常困惑。它给出了该模型的示例:

c.query(
   'SELECT SLEEP(20);',
   function (err, results, fields) {
     if (err) {
       throw err;
     }
     res.writeHead(200, {'Content-Type': 'text/html'});
     res.end('<html><head><title>Hello</title></head><body><h1>Return from async DB query</h1></body></html>');
     c.end();
    }
);

队列:由于只有一个线程,所以有两个请求A(首先出现)和B(首先出现),服务器端程序将首先处理请求A:执行SQL查询是代表I / O等待的睡眠语句。并且该程序被困在I/O等待中,并且无法执行使网页落后的代码。在等待期间程序会切换到请求B吗?我认为,由于是单线程模型,因此无法将一个请求与另一个请求进行切换。但是示例代码的标题表明,除了您的代码之外所有其他内容都可以并行运行

(由于我从没使用过Node,所以我不确定我是否会误解代码。)在等待期间,Node如何将A切换到B?您能否以简单的方式解释Node的单线程无阻塞IO模型?如果您能帮助我,我将不胜感激。:)

Answers:


374

Node.js建立在libuv之上,libuv是一个跨平台的库,为支持的操作系统(至少为Unix,OS X和Windows)提供的异步(非阻塞)输入/输出抽象api / syscall。

异步IO

在此编程模型中,对由文件系统管理的设备和资源(套接字,文件系统等)的打开/读取/写入操作不会阻塞调用线程(就像在典型的同步C样模型中一样),而只是标记新数据或事件可用时,将通知进程(在内核/ OS级数据结构中)。如果是类似Web服务器的应用程序,则该过程负责确定通知事件属于哪个请求/上下文,并从那里继续处理请求。请注意,这必然意味着您将与向OS发出请求的堆栈位于不同的堆栈框架上,因为OS必须屈服于进程的调度程序才能使单个线程的进程处理新事件。

我描述的模型的问题在于,它对程序员不熟悉并且很难推理,因为它本质上是非顺序的。“您需要在函数A中发出请求,并在另一个函数中处理结果,而在该函数中,通常无法使用来自A的本地人。

节点的模型(继续传递样式和事件循环)

Node通过诱使程序员采用某种编程风格,利用javascript的语言功能解决了该问题,使该模型看起来更具同步性。每个请求IO的函数都具有类似的签名,function (... parameters ..., callback)并且需要给其提供一个回调,该回调将在请求的操作完成时被调用(请注意,大部分时间都在等待OS发出完成信号,这是可以花费的时间)。花了其他工作)。Javascript对闭包的支持使您可以使用在回调主体内部的外部(调用)函数中定义的变量-这可以保留节点运行时将独立调用的不同函数之间的状态。另请参见继续传递样式

而且,在调用产生了IO操作的函数之后,调用函数通常将return控制到节点的事件循环。该循环将调用计划执行的下一个回调或函数(很可能是因为相应的事件已由OS通知)-这允许并发处理多个请求。

您可以认为节点的事件循环有点类似于内核的调度程序:内核将在其未完成的IO完成后调度执行阻塞的线程,而节点将在发生相应的事件时调度回调。

高度并发,无并行

最后,短语“除了代码之外的所有内容并行运行”在捕获节点这一点方面做得很不错,该节点允许您的代码通过多路复用和排序所有js并通过一个线程同时处理来自成千上万个开放套接字的请求单个执行流中的逻辑(即使说“一切并行运行”在这里可能不正确-参见并发与并行性-有什么区别?)。这对于webapp服务器非常有效,因为实际上大部分时间都花在等待网络或磁盘(数据库/套接字)上,并且逻辑实际上并不占用大量CPU,也就是说:这对于IO绑定的工作负载非常有效


45
后续问题:I / O实际如何发生?节点正在向系统发出请求,并要求在完成时收到通知。那么系统是在运行正在执行I / O的线程,还是系统也在使用中断在硬件级别异步执行I / O?某处必须等待I / O完成,这将阻塞直到完成,并消耗大量资源。
菲利普(Philip)

6
刚刚注意到此后续评论由下面的@ user568109回答,我希望有一种合并这两个答案的方法。
lfalin 2014年

4
我希望您能写出两倍的回复,所以我会理解得更好。
拉斐尔·伊恩

记录在很多地方都支持Node。当我为MIPS32路由器设计固件时,可以通过OpenWRT在其中运行Node.JS。
Qix-蒙尼卡(Monica)

与Apache相比得分如何?Apache还能够使用单独的线程处理并发连接。
Suhail Gupta'5

210

好吧,给出一些观点,让我比较一下node.js和apache。

Apache是​​一个多线程HTTP服务器,对于服务器收到的每个请求,它都会创建一个单独的线程来处理该请求。

另一方面,Node.js是事件驱动的,从单个线程异步处理所有请求。

当在apache上收到A和B时,将创建两个线程来处理请求。每个都分别处理查询,每个都在为页面提供服务之前等待查询结果。该页面仅在查询完成之前提供。查询提取被阻止,因为服务器直到收到结果后才能执行其余线程。

在节点中,c.query是异步处理的,这意味着,当c.query获取A的结果时,它会跳转以处理B的c.query,当结果到达A时,它将结果发送回回调,该回调将发送响应。Node.js知道在提取完成时执行回调。

我认为,由于它是单线程模型,因此无法从一个请求切换到另一个请求。

实际上,节点服务器一直在为您执行此操作。为了进行切换,(异步行为)您将使用的大多数函数都将具有回调。

编辑

SQL查询取自mysql库。它实现了回调样式以及事件发射器来对SQL请求进行排队。它不会异步执行它们,这是由提供非阻塞I / O抽象的内部libuv线程完成的。进行查询的步骤如下:

  1. 打开与db的连接,连接本身可以异步进行。
  2. 连接数据库后,查询将传递到服务器。查询可以排队。
  3. 主事件循环通过回调或事件获得完成通知。
  4. 主循环执行您的回调/事件处理程序。

以类似的方式处理对http服务器的传入请求。内部线程体系结构是这样的:

node.js事件循环

C ++线程是libuv线程,它们执行异步I / O(磁盘或网络)。在将请求分派到线程池之后,主事件循环继续执行。它可以等待或休眠,因此可以接受更多请求。SQL查询/ HTTP请求/文件系统读取都是以这种方式发生的。


16
图表非常有用。
2014年

14
等等,所以在您的图表中,您具有“内部C ++线程池”,这意味着所有IO阻塞操作都将产生一个线程,对吗?因此,如果我的Node应用程序为每个请求执行一些IO ,那么Node模型和Apache模型之间实际上没有区别吗?我没有让这部分对不起。
gav.newalkar,2015年

21
@ gav.newalkar他们不产生线程,请求被排队。线程池中的线程处理它们。线程不是动态的,并且每个请求都不像Apache中那样。它们通常是固定的,并且因系统而异。
user568109

10
@ user568109但是Apache使用线程池太(httpd.apache.org/docs/2.4/mod/worker.html)。因此,最终,使用node.js进行设置的区别与仅使用线程池所在的Apache进行了区别,不是吗?
克里斯(Kris)2016年

13
该图应在官方文档的第一页上。
bouvierr

52

Node.js 在后台使用libuv。libuv 有一个线程池(默认情况下大小为4)。因此,Node.js 确实使用线程来实现并发。

但是您的代码在单个线程上运行(即,将在同一线程上调用Node.js函数的所有回调,即所谓的循环线程或事件循环)。当人们说“ Node.js在单个线程上运行”时,他们实际上是在说“ Node.js的回调在单个线程上运行”。


1
简短但明确的答案(y)
Sudhanshu Gaur

1
好答案,我想补充一点,I / O发生在此主事件循环,循环线程,请求线程之外
Ionut Popa

那就是我从2个小时开始搜索的答案,即如何在单线程应用程序中管理并发性
Muhammad Ramzan

是的,很难获得“下一级”的答案。这说明了IO实际完成的位置(在其他地方的线程池中)
Oliver Shaw

9

Node.js基于事件循环编程模型。事件循环在单线程中运行,并反复等待事件,然后运行订阅这些事件的所有事件处理程序。事件可以是例如

  • 计时器等待完成
  • 下一个数据块已准备好写入此文件
  • 有一个新的HTTP请求即将到来

所有这些都在单线程中运行,并且从来没有并行执行JavaScript代码。只要这些事件处理程序很小,并且等待更多事件本身,一切就可以顺利进行。这允许单个Node.js进程同时处理多个请求。

(事件发生的源头有点不可思议。其中一些涉及并行运行的低级工作线程。)

在这种SQL情况下,在进行数据库查询与在callback中获取结果之间发生了很多事情(事件)。在这段时间内,事件循环不断为应用程序注入生命,并一次将一个小事件推进到其他请求中。因此,同时处理多个请求。

事件循环高级视图

根据:“来自10,000英尺的事件循环-Node.js背后的核心概念”


5

函数c.query()有两个参数

c.query("Fetch Data", "Post-Processing of Data")

在这种情况下,“获取数据”操作是一个数据库查询,现在可以由Node.js处理,方法是派生一个工作线程并将执行数据库查询的任务赋予它。(请记住,Node.js可以在内部创建线程)。这使函数可以立即返回而没有任何延迟

第二个参数“数据的后处理”是一个回调函数,节点框架注册该回调并由事件循环调用。

因此,该语句c.query (paramenter1, parameter2)将立即返回,从而使节点能够满足另一个请求。

PS:我才刚刚开始了解节点,实际上我想将其写为@Philip的注释, 但由于没有足够的声誉点,因此将其写为答案。


3

如果您进一步阅读-“当然,在后端,有一些线程和进程用于数据库访问和进程执行。但是,这些线程和进程没有显式地暴露给您的代码,因此您不必担心它们,除非知道从每个请求的角度来看,与数据库或其他进程的I / O交互将是异步​​的,因为这些线程的结果是通过事件循环返回到您的代码的。”

关于-“除了代码之外的所有内容并行运行”-您的代码是同步执行的,每当您调用异步操作(如等待IO)时,事件循环都会处理所有内容并调用回调。它只是您不必考虑的事情。

在您的示例中:有两个请求A(首先出现)和B。您执行请求A,您的代码继续同步运行并执行请求B。事件循环处理请求A,当它完成时,它调用请求A的回调结果,同样转到请求B。


3
“当然,在后端,有一些线程和进程用于数据库访问和进程执行。但是,这些线程和进程没有显式地暴露给您的代码。” -如果我从这句话中得出,那么我看不出Node的区别。或任何多线程框架-假设Java的Spring框架-都可以。有线程,但是您不能控制它们的创建。
拉斐尔·伊恩

@RafaelEyng我认为要处理一系列的多个请求,node总是会有一个线程。我不确定是否每个回调都放在除数据库访问等其他进程之外的线程的新实例上,但是至少我们可以肯定地知道,节点不会在每次收到请求时都实例化线程,而该请求必须在处理之前排队(执行之前回调)。
Cold Cerberus

1

好的,到目前为止,大多数事情应该都清楚了…… 最棘手的部分是SQL:如果实际上不是在另一个线程或整个进程运行,则必须将SQL执行分解为各个步骤(通过SQL处理器是为异步执行而设计的!),其中执行非阻塞线程,而阻塞线程(例如睡眠)实际上可以传输到内核(作为警报中断/事件),并放置在事件列表中。主循环。

这就是说,例如,SQL的解释等是立即完成的,但是在等待期间(由内核以某种kqueue,epoll,...结构存储为将来要发生的事件;以及其他IO操作)主循环可以做其他事情,并最终检查这些IO是否发生了什么并等待。

因此,再次改写一下:程序永远不会(允许被阻塞),永远不会执行睡眠调用。它们的职责是由内核(写一些东西,等待一些东西通过网络,等待时间过去)或另一个线程或进程来完成的。–在每个事件循环周期中,Node进程仅在对OS的唯一阻塞调用中检查内核是否至少完成了其中一项职责。完成所有非阻塞操作后,即达到了这一点。

明确?:-)

我不知道Node。但是c.query是从哪里来的呢?


kqueue epoll用于Linux内核中的可伸缩异步I / O通知。Node具有libuv。节点完全位于用户区。它不依赖于内核实现什么。
user568109

1
@ user568109,libuv是Node的中间人。任何异步框架都(直接或不直接)依赖于内核中的某些异步I / O支持。所以?
罗伯·席默

对困惑感到抱歉。套接字操作需要来自内核的非阻塞I / O。它负责异步处理。但是异步文件I / O由libuv本身处理。您的答案并不能说明问题。内核将其视为相同。
user568109
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.