342

我不是 Node 程序员,但我对单线程非阻塞 IO 模型的工作原理很感兴趣。在我阅读了理解-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模型吗?如果您能帮助我,我将不胜感激。:)

4

8 回答 8

394

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

异步 IO

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

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

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

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

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

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

高并发,无并行

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

于 2013-02-10T11:41:36.507 回答
219

好吧,为了给出一些观点,让我将 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线程完成的。进行查询时会发生以下步骤:

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

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

node.js 事件循环

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

于 2013-02-10T11:09:51.653 回答
58

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

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

于 2016-11-28T19:56:16.630 回答
9

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

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

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

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

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

事件循环高级视图

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

于 2015-09-14T07:51:52.653 回答
6

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

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

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

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

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

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

于 2014-06-09T09:43:22.810 回答
3

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

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

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

于 2013-02-10T10:25:19.013 回答
1

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

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

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

清除?:-)

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

于 2013-11-09T10:50:18.497 回答
0

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 个将等待。一旦每个请求被执行,结果就会返回给`event demultiplexer。

在此处输入图像描述

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

在此处输入图像描述

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

于 2022-01-27T03:55:50.503 回答