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

内容来源于 Stack Overflow,并遵循CC BY-SA 3.0许可协议进行翻译与使用

  • 回答 (7)
  • 关注 (0)
  • 查看 (398)

我不是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。)

在等待期间节点如何将A切换到B?你能解释单线程非阻塞IO模型Node一个简单的方法?

提问于
用户回答回答于

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

异步IO

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

我所描述的模型的问题在于,对于程序员而言,由于它是非顺序的,所以并不熟悉和难以推断。“您需要在功能A中提出请求,并将结果处理为不同的功能,A中的当地人通常不可用。”

节点的模型(Continuation Passing Style and Event Loop)

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

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

您可以将节点的事件循环看作与内核的调度程序有些类似:内核会在其未完成的IO完成后计划执行阻塞的线程,而节点将在发生相应的事件时安排回调。

高度并发,不并行

作为最后一句话,“除了你的代码之外,所有东西都是并行运行的”这样一个体面的工作就是捕获节点允许你的代码同时处理来自成千上万个开放套接字的请求的同时通过复用和排序你所有的js逻辑在一个单一的执行流(即使说“一切平行运行”在这里可能是不正确的 - 看并发与并行 - 有什么区别?)。这对于Web应用程序服务器来说工作得非常好,因为大部分时间实际上是花在等待网络或磁盘(数据库/套接字)上的,逻辑并不是真正的CPU密集型 - 也就是说:这对IO绑定的工作负载很好

用户回答回答于

到目前为止大多数事情都很清楚...最棘手的部分是sql。如果这不是在另一个线程或进程中运行则非阻塞的执行在整个过程中,sql-执行必须是分成几个步骤(通过为异步执行而设计的sql处理器!),而阻塞的执行(例如睡眠),转移到内核(作为警报中断/事件),并将其放到主循环的事件列表中。

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

因此,要重新定义它:程序永远不会(允许)卡住,休眠调用永远不会执行。它们的任务由内核(编写一些东西、等待通过网络的东西、等待时间流逝)或另一个线程或进程来完成。Node进程检查这些任务中是否至少有一个由内核在每个事件循环周期中对操作系统的唯一阻塞调用中完成。当所有非阻塞的事情都完成时,这一点就达到了。

用户回答回答于

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

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

在您的示例中:有两个请求A(先来)和B。您执行请求A,您的代码继续同步运行并执行请求B。事件循环处理请求A,当它完成对请求A的回调时,结果也是如此。

用户回答回答于

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

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

在本例中,“Fetch Data"操作是一个DB查询,现在这可以由Node.js通过生成一个工作线程并赋予它执行DB-Query的任务来处理。(记住Node.js可以在内部创建线程)。这使函数能够立即返回,而不需要任何延迟。

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

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

用户回答回答于

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

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

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

(事情发生的地方有一点奇怪。其中有些涉及并行运行的低级工作线程。)

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

用户回答回答于

Node.js在后台使用 libuv库。 libuv库有一个线程池(默认情况下为4)。因此,node.js使用多线程实现并发。

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

用户回答回答于

为了说明不同,我把node.js与Apache放在一起进行比较。

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

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

当在Apache上接收到A和B时,将创建两个线程来处理请求。每个单独处理查询,每个在服务页面之前等待查询结果。只有在查询完成之前才能使用该页面。由于服务器在收到结果之前无法执行其余的线程,所以查询FETCH正在阻塞。

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

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

实际上,节点服务器一直都是这样做的。要使开关(异步行为),您要使用的大多数函数都会有回调。

编辑

SQL查询从Mysql library。它实现了回调样式以及事件发射器来排队SQL请求。它不异步执行它们,这是由internal libuv提供非阻塞I/O抽象的线程。进行查询时会执行以下步骤:

  1. 打开到db的连接,就可以异步地建立连接本身。
  2. 连接db后,查询将传递给服务器。查询可以排队。
  3. 主事件循环将通过回调或事件得到完成通知。
  4. 主循环执行回调/均衡器。

对http服务器的传入请求以类似的方式处理。内部线程体系结构如下所示:

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

扫码关注云+社区

领取腾讯云代金券