单线程非阻塞IO模型如何在Node.js中工作

我不是一个Node程序员,但我对单线程非阻塞IO模型如何工作感兴趣。 在我读了文章understanding-the-node-js-event-loop之后,我真的很困惑。 它给出了一个模型示例:

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吗?在我看来,由于单线程模型,没有办法从一个请求切换到另一个请求。但是示例代码的标题说除了代码之外,所有东西都是并行运行的. c。

< p > (p。我不确定我是否误解了代码,因为我已经理解了 从未使用过Node。)在等待过程中,节点如何将A切换到B ?并且可以 你在a中解释Node的单线程非阻塞IO模型 简单的方法吗?如果你能帮助我,我会很感激的。:) < / p >
114140 次浏览

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

about - "除了你的代码,所有东西都是并行运行的" -你的代码是同步执行的,每当你调用一个异步操作,比如等待IO,事件循环处理所有事情并调用回调。这不是你需要考虑的事情。

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

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

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

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

当在apache上接收到A和B时,会创建两个线程来处理请求。每个服务器分别处理查询,每个服务器在提供页面之前等待查询结果。该页面只在查询完成之前提供。查询获取阻塞是因为服务器在收到结果之前无法执行线程的其余部分。

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

在我看来,因为它是一个单线程模型,没有办法

.从一个请求切换到另一个请求

实际上,节点服务器一直都在为你做这件事。要进行切换,(异步行为)您将使用的大多数函数将具有回调。

编辑

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

  1. 打开一个到db的连接,连接本身可以异步进行。
  2. 一旦连接db,查询就被传递到服务器。查询可以排队。
  3. 主事件循环通过回调或事件得到完成通知。
  4. 主循环执行回调/事件处理程序。

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

node.js event loop

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

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

异步IO

在这个编程模型中,由文件系统不要阻塞调用线程管理的设备和资源(套接字、文件系统等)上的打开/读/写操作(就像在典型的同步类c模型中一样),只是在新数据或事件可用时标记进程(在内核/操作系统级数据结构中),以便通知。对于类似web服务器的应用程序,该进程负责找出通知事件属于哪个请求/上下文,并从那里继续处理请求。请注意,这将意味着您将处于与向操作系统发起请求的堆栈框架不同的堆栈框架上,因为后者必须屈服于进程的调度程序,以便单线程进程处理新事件。

我所描述的模型的问题在于,它对程序员来说并不熟悉,而且很难推理,因为它本质上是非顺序的。“你需要在函数A中提出请求,并在不同的函数中处理结果,而来自A的局部变量通常不可用。”

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

Node利用javascript的语言特性解决了这个问题,通过诱导程序员采用某种编程风格,使这个模型看起来更具同步性。每个请求IO的函数都有一个像function (... parameters ..., callback)这样的签名,并且需要被赋予一个回调,当请求的操作完成时将被调用(请记住,大部分时间都花在等待操作系统发出完成信号上——时间可以花在其他工作上)。Javascript对闭包的支持允许你在回调函数体中使用你在外部(调用)函数中定义的变量——这允许在不同的函数之间保持状态,这些函数将由节点运行时独立调用。另见延续传球风格

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

你可以把节点的事件循环想象成有点类似于内核的调度程序:内核会在阻塞线程的暂挂IO完成后安排执行线程,而节点会在相应事件发生时安排回调。

高度并发,无并行性

作为最后的评论,短语“除了你的代码之外,所有的东西都并行运行”很好地抓住了这一点,即节点允许你的代码通过多路复用和在单个执行流中排序所有js逻辑并发地处理来自成千上万的开放插座与一个单一的线程的请求(即使说“所有的东西都并行运行”在这里可能不正确-请参见并发和并行——有什么区别?)。这对于web应用服务器来说工作得很好,因为大部分时间实际上都花在等待网络或磁盘(数据库/套接字)上,而且逻辑不是真正的CPU密集型——也就是说:这适用于io绑定的工作负载

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

这意味着,例如,SQL的解释,等等是立即完成的,但在等待期间(存储为一个事件,将来由内核在一些kqueue, epoll,…结构;与其他IO操作一起),主循环可以做其他事情,并最终检查这些IOs是否发生了什么并等待。

所以,换句话说:程序永远不会(被允许)卡住,休眠调用永远不会被执行。它们的职责是由内核(写一些东西,等待一些东西通过网络,等待时间流逝)或另一个线程或进程完成的。- Node进程检查在每个事件循环周期中,内核是否在唯一的阻塞调用中完成了至少一项任务。当所有非阻塞的事情都完成时,就达到了这一点。

清楚了吗?: -)

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

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

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

在这种情况下,“获取数据”操作是一个DB-Query,现在这可能由Node.js通过衍生出一个工作线程来处理,并赋予它执行DB-Query的任务。(记住Node.js可以在内部创建线程)。这使得函数能够立即返回,没有任何延迟

第二个参数“Post-Processing of Data”是一个回调函数,节点框架注册这个回调函数,并由事件循环调用。

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

附注:我刚刚开始理解节点,实际上我想把它作为@Philip的注释,但由于没有足够的声誉点,所以把它作为答案写了下来。

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

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

所有这些都在单个线程中运行,没有JavaScript代码是并行执行的。只要这些事件处理程序很小,并且等待更多的事件本身,一切都能很好地工作。这允许单个Node.js进程并发处理多个请求。

(在事件起源的地方有一点魔力。其中一些涉及并行运行的低级工作线程。)

在这个SQL例子中,在进行数据库查询和在回调中获得结果之间发生了很多事情(事件)。在此期间,事件循环不断向应用程序注入生命,并一次一个小事件推进其他请求。因此,并发地服务多个请求。

event loop high level view

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

Node.js在幕后使用libuv。libuv 有一个线程池(默认大小为4)。因此Node.js 使用线程实现并发。

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

event loop允许Node.js执行非阻塞I/O操作——尽管JavaScript是单线程的——只要有可能就将操作卸载到系统内核。将event loop视为管理器。

  • 新的请求被发送到队列中并由synchronous event demultiplexer监视。如您所见,每个操作处理程序也已注册。

enter image description here

  • 然后这些请求被同步发送到线程池(Worker pool)执行。JavaScript不能执行异步I/O操作。在浏览器环境中,浏览器处理异步操作。在节点环境中,异步操作由libuv通过使用C++来处理。线程池的默认大小是4,但是可以在启动时通过将UV_THREADPOOL_SIZE环境变量设置为任何值(最大为128)来改变它。线程池大小为4意味着一次可以执行4个请求,如果事件解复用器有5个请求,那么4个将被传递到线程池,第5个将等待。一旦每个请求被执行,结果就会返回到事件解复用器。

enter image description here

  • 当一组I/O操作完成时,事件解复用器将一组对应的事件推入事件队列。

enter image description here

Handler是回调。现在,事件循环监视事件队列,如果有准备好的事件,它将被推入堆栈以执行回调。记住,回调最终是在堆栈上执行的。请注意,一些回调对其他回调具有优先级,事件循环确实根据它们的优先级选择回调。

对于那些寻求简单答案而不想深入Node.js内部的人。

Node.js不是单线程的,它默认运行在5个线程上。

是的,只有一个线程用于实际的JavaScript处理,但它总是在函数之间切换。

它将SQL查询发送到数据库,并让它在其他线程中等待,而单线程Node.js继续计算一些准备计算的其他代码。

如果你想要更多的解释,有关于事件循环工人池整个libuv文档的好文章。