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

我不是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等待的sleeping语句。并且该程序被困在I/O等待中,并且无法执行使网页落后的代码。程序在等待期间会切换到请求B吗?我认为,由于是单线程模型,因此无法将一个请求与另一个请求进行切换。但是示例代码的标题表明,除了您的代码之外所有其他内容都可以并行运行

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

YOC644217922020/03/18 16:55:11

Okay, most things should be clear so far... the tricky part is the SQL: if it is not in reality running in another thread or process in it’s entirety, the SQL-execution has to be broken down into individual steps (by an SQL processor made for asynchronous execution!), where the non-blocking ones are executed, and the blocking ones (e.g. the sleep) actually can be transferred to the kernel (as an alarm interrupt/event) and put on the event list for the main loop.

That means, e.g. the interpretation of the SQL, etc. is done immediately, but during the wait (stored as an event to come in the future by the kernel in some kqueue, epoll, ... structure; together with the other IO operations) the main loop can do other things and eventually check if something happened of those IOs and waits.

So, to rephrase it again: the program is never (allowed to get) stuck, sleeping calls are never executed. Their duty is done by the kernel (write something, wait for something to come over the network, waiting for time to elapse) or another thread or process. – The Node process checks if at least one of those duties is finished by the kernel in the only blocking call to the OS once in each event-loop-cycle. That point is reached, when everything non-blocking is done.

Clear? :-)

I don’t know Node. But where does the c.query come from?

TomGO小小2020/03/18 16:55:11

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

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

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

NearJinJin2020/03/18 16:55:11

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

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

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

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

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

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

HarryTony2020/03/18 16:55:11

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绑定的工作负载非常有效

路易LEY2020/03/18 16:55:11

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

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

Tony西门2020/03/18 16:55:11

好吧,给出一些观点,让我比较一下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请求/文件系统读取都是以这种方式发生的。

神无2020/03/18 16:55:11

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

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

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

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

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

事件循环高级视图

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