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

Posted

技术标签:

【中文标题】单线程非阻塞 IO 模型如何在 Node.js 中工作【英文标题】:How the single threaded non blocking IO model works in Node.js 【发布时间】:2013-01-25 13:14:07 【问题描述】:

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

(PS 我不确定我是否误解了代码,因为我有 从未使用过Node。)在等待期间Node如何将A切换到B?并且可以 您在一篇文章中解释了 Node 的单线程非阻塞 IO 模型 简单的方法?如果您能帮助我,我将不胜感激。 :)

【问题讨论】:

【参考方案1】:

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

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

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

【讨论】:

“当然,在后端,有用于数据库访问和进程执行的线程和进程。但是,这些不会显式暴露给您的代码” - 如果我采取从这句话来看,我看不出 Node 所做的事情或任何多线程框架(比如 Java 的 Spring 框架)所做的事情之间有什么区别。有线程,但您无法控制它们的创建。 @RafaelEyng 我认为为了处理一系列多个请求,节点将始终拥有一个线程。我不确定除了数据库访问等其他进程之外,每个回调是否都放在新的线程实例上,但至少我们肯定知道节点不会在每次收到请求时实例化线程,该请求必须在处理之前排队等待(执行之前回调)。【参考方案2】:

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

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

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

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

在node中,c.query是异步处理的,也就是说c.query在为A获取结果的同时,会跳转到为B去处理c.query,当A的结果到达时,它会将结果返回给回调发送响应。 Node.js 知道在 fetch 完成时执行回调。

在我看来,因为它是单线程模型,所以没有办法 从一个请求切换到另一个请求。

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

编辑

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

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

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

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

【讨论】:

等等,所以在你的图中你有“内部 C++ 线程池”,这意味着所有的 IO 阻塞操作都会产生一个线程,对吧?因此,如果我的 Node 应用程序对每个请求都进行了一些 IO 工作,那么 Node 模型和 Apache 模型之间是否几乎没有区别?对不起,我没有得到这部分。 @gav.newalkar 他们不产生线程,请求排队。线程池中的线程处理它们。线程不是动态的,并且与 Apache 中的每个请求不同。它们通常是固定的,并且因系统而异。 @user568109 但是 Apache 也在使用线程池 (httpd.apache.org/docs/2.4/mod/worker.html)。所以说到底,node.js 的设置和前面 Apache 的区别只在于线程池所在的位置,不是吗? @user568109 如果请求数多于 c++ 线程池的线程数会怎样?为什么node的单线程事件循环不阻塞? @Kris 是的,Apache 也在使用线程池。但是,所有处理(即 SQL 查询本身以及从数据库返回结果后接下来会发生什么)都是在同一个线程上完成的。 Node.js 将仅在单独的线程(来自 libuv 线程池的线程)上执行查询部分,并将结果传递到事件循环线程上。因此,回调中的代码将在事件循环线程上执行。【参考方案3】:

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

异步 IO

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

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

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

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

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

你可以把node的事件循环想象成有点类似于内核的dispatcher:内核会在等待IO完成后调度阻塞线程执行,而node会在对应事件发生时调度回调已经发生了。

高并发,无并行

最后一句话,“除了你的代码之外,一切都并行运行”这句话很好地捕捉到了节点允许你的代码处理来自数十万个单线程打开套接字的请求的点 em> 通过在单个执行流中对所有 js 逻辑进行多路复用和排序(即使在这里说“一切都并行运行”可能不正确 - 请参阅Concurrency vs Parallelism - What is the difference?)。这对于 webapp 服务器非常有效,因为大部分时间实际上都花在等待网络或磁盘(数据库/套接字)上,并且逻辑并不是真正的 CPU 密集型 - 也就是说:这适用于 IO-bound工作负载

【讨论】:

后续问题:I/O 是如何实际发生的? Node 正在向系统发出请求,并要求在完成时得到通知。那么系统是否正在运行一个正在执行 I/O 的线程,或者系统是否也在使用中断在硬件级别异步执行 I/O?某处必须等待 I/O 完成,这将阻塞直到它完成并消耗一些资源。 刚刚注意到这个后续评论是由下面的@user568109 回答的,我希望有办法合并这两个答案。 Node 很多地方都支持,记录一下。当我为 MIPS32 路由器设计固件时,Node.JS 可以通过 OpenWRT 在那些路由器上运行。 @Philip 有一种方法永远不需要轮询。阅读有关硬件中断的信息。像磁盘这样的设备(使用文件处理程序作为以太网适配器等实际物理接口的代表)可以通过硬件中断向操作系统发出信号,表明它已准备好接收一些数据。***en.wikipedia.org/wiki/Asynchronous_I/O 说“..直接内存访问 (DMA) 可以大大提高基于轮询的系统的效率,而硬件中断可以完全消除轮询的需要......”。 @utaal,当你说“节点的事件循环”时,它与“JS事件循环”有什么不同吗?原因 JS 还使用“事件循环”来处理“setTimeOut”等内容。如本视频中关于“JS 事件循环”youtu.be/8aGhZQkoFbQ 所述【参考方案4】:

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

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

所以,重新表述一下:程序永远不会(允许卡住),睡眠调用永远不会执行。他们的职责是由内核(写一些东西,等待一些东西通过网络,等待时间过去)或另一个线程或进程来完成的。 – Node 进程检查内核是否在每个事件循环周期中对操作系统的唯一阻塞调用中完成了这些职责中的至少一项。当所有非阻塞都完成时,就达到了这一点。

清楚吗? :-)

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

【讨论】:

kqueue epoll 用于 linux 内核中可扩展的异步 I/O 通知。 Node为此提供了libuv。节点完全在用户空间。它不依赖于内核实现的内容。 @user568109,libuv 是 Node 的中间人。任何异步框架都(直接或不)依赖于内核中的一些异步 I/O 支持。那么? 很抱歉给您带来了困惑。套接字操作需要来自内核的非阻塞 I/O。它负责异步处理。但是异步文件 I/O 由 libuv 自己处理。你的回答没有说​​明这一点。它将两者视为相同,由内核处理。【参考方案5】:

函数 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) 语句将立即返回,使节点能够满足另一个请求。

P.S:我刚刚开始了解节点,实际上我想将其写为对@Philip 的评论,但由于没有足够的声誉点,所以将其写为答案。

【讨论】:

【参考方案6】:

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

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

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

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

在这个 SQL 案例中,在进行数据库查询和在回调中获取结果之间发生了很多事情(事件)。在此期间,事件循环不断为应用程序注入生命,并一次推进其他请求一个微小的事件。因此同时处理多个请求。

根据:"Event loop from 10,000ft - core concept behind Node.js".

【讨论】:

【参考方案7】:

Node.js 在幕后使用libuv。 libuv has a thread pool(默认大小为 4)。因此 Node.js 确实使用线程来实现并发。

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

【讨论】:

好答案我要补充一点,I/O 发生在这个主事件循环、循环线程、请求线程之外【参考方案8】:

event loop 允许 Node.js 通过尽可能将操作卸载到系统内核来执行非阻塞 I/O 操作(尽管 JavaScript 是单线程的)。将event loop 视为经理。

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

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

当一组 I/O 操作完成时,事件解复用器将一组相应的事件推送到事件队列中。

handler 是回调。现在事件循环关注事件队列,如果有东西准备好,它被推入堆栈执行回调。请记住,回调最终会在堆栈上执行。请注意,某些回调具有其他优先级,事件循环确实会根据它们的优先级选择回调。

【讨论】:

以上是关于单线程非阻塞 IO 模型如何在 Node.js 中工作的主要内容,如果未能解决你的问题,请参考以下文章

Redis线程IO模型的秘密知多少

IO流中「线程」模型总结

node_简介及部署安装配置

这就是所谓的Node.js------单线程,非阻塞,事件驱动

node.js基础

Tornado的异步非阻塞