ChatGPT解决这个技术问题 Extra ChatGPT

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

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


C
Community

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

异步 IO

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

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

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

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

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

您可以将 node 的事件循环视为有点类似于内核的调度程序:一旦其挂起的 IO 完成,内核将调度执行一个阻塞线程,而 node 将在相应事件发生时调度回调。

高并发,无并行

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


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

好吧,为了给出一些观点,让我将 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 的连接,连接本身可以异步进行。连接 db 后,查询将传递到服务器。查询可以排队。主事件循环通过回调或事件获得完成通知。主循环执行您的回调/事件处理程序。

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

https://i.stack.imgur.com/YCTgK.png

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++ 线程池的线程数会怎样?为什么节点的单线程事件循环不阻塞?
@Kris 是的,Apache 也在使用线程池。但是,所有处理(即 SQL 查询本身以及从数据库返回结果后接下来会发生什么)都是在同一个线程上完成的。 Node.js 将仅在单独的线程(来自 libuv 线程池的线程)上执行查询部分,并将结果传递到事件循环线程上。因此,回调中的代码将在事件循环线程上执行。
B
Bitcoin Cash - ADA enthusiast

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

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


好的答案我要补充一点,I/O 发生在这个主事件循环、循环线程、请求线程之外
C
Community

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

定时器等待完成

下一块数据已准备好写入此文件

有一个全新的 HTTP 请求即将到来

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

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

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

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


C
Community

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

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

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

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

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

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


G
Gal Ben-Haim

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

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

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


“当然,在后端,有用于数据库访问和进程执行的线程和进程。但是,这些并没有显式地暴露给你的代码”——如果我从这句话中理解,那么我看不出 Node 之间有什么区别do 或任何多线程框架 - 比如说 Java 的 Spring Framework - 都可以。有线程,但您无法控制它们的创建。
@RafaelEyng 我认为为了处理一系列多个请求,节点将始终有一个线程。我不确定除了数据库访问等其他进程之外,每个回调是否都放在新的线程实例上,但至少我们肯定知道节点不会在每次收到请求时实例化线程,该请求必须在处理之前排队等待(执行之前回调)。
R
Robert Siemer

好的,到目前为止,大多数事情都应该清楚了......棘手的部分是 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 自己处理。你的回答没有说明这一点。它将两者视为相同,由内核处理。
Y
Yilmaz

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

新请求被发送到队列中并由同步事件解复用器监视。如您所见,每个操作处理程序也已注册。

https://i.stack.imgur.com/EgloV.png

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

https://i.stack.imgur.com/WtWHm.png

当一组 I/O 操作完成时,Event Demultiplexer 将一组相应的事件推送到 Event Queue 中。

https://i.stack.imgur.com/S8fGU.png

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