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

Wen*_* Ji 323 node.js

我不是Node程序员,但我对单线程非阻塞IO模型的工作原理感兴趣.在我阅读了文章理解-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();
    }
);
Run Code Online (Sandbox Code Playgroud)

这是一个问题.当有两个请求A(至上)和B,由于只有一个线程,服务器端程序将处理请求的第一:做SQL查询这是一个睡眠声明站立I/O等待.并且程序停留在I/O等待,并且无法执行呈现网页的代码.程序会在等待期间切换到请求B吗?在我看来,由于单线程模型,没有办法从一个请求切换另一个请求.但是示例代码的标题说"除了代码之外,所有内容都是并行运行".(PS我不确定我是否误解了代码,因为我从未使用过Node.)节点如何在等待期间切换到B?你能用一种简单的方法解释Node的单线程非阻塞IO模型吗?如果你能帮助我,我将不胜感激.:)

Uta*_*aal 370

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

异步IO

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

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

节点的模型(延续传递风格和事件循环)

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

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

您可以将节点的事件循环视为与内核的调度程序有些类似:内核将在其挂起的IO完成后计划执行被阻塞的线程,而节点将在相应的事件发生时调度回调.

高度并发,没有并行性

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

  • 后续问题:I/O实际上是如何发生的?节点正在向系统发出请求,并要求在完成后通知系统.那么系统运行的是执行I/O的线程,还是系统还使用中断在硬件级别异步执行I/O?某个地方必须等待I/O完成,这将阻止它完成并消耗一些资源. (44认同)
  • 刚刚注意到这个后续评论由@ user568109回答,我希望有一种方法可以合并这两个答案. (6认同)
  • 我希望你能写两次回复,所以我会更好地理解两次. (4认同)

use*_*109 204

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

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

node.js事件循环

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

  • @ gav.newalkar他们没有产生一个线程,请求排队.线程池中的线程处理它们.线程不是动态的,也不是Apache中的每个请求.它们通常是固定的,因系统而异. (21认同)
  • 图非常有用. (16认同)
  • 等等,所以在你的图表中你有"内部C++线程池",这意味着所有IO阻塞操作都会产生一个线程,对吧?因此,如果我的Node应用程序为每个请求*执行一些IO工作,那么Node模型和Apache模型之间几乎没有区别吗?我没有得到这部分抱歉. (13认同)
  • 该图应该在官方文档的第一页上. (13认同)
  • @ user568109但Apache也在使用线程池(https://httpd.apache.org/docs/2.4/mod/worker.html).所以最后,node.js的设置与前面的Apache的设置之间的区别仅在于线程池所在的位置,不是吗? (9认同)
  • 对.带有libuv的Node.js不仅仅是一个整体线程. (7认同)
  • @ user568109如果有多个请求比c ++线程池有线程会怎么样?为什么节点的单线程事件循环阻塞? (5认同)
  • @Kris是的,Apache也在使用线程池.但是,所有处理(即SQL查询本身以及从数据库返回结果后接下来发生的事情)都在同一个线程上完成.Node.js将仅在一个单独的线程(来自libuv的线程池的线程)上执行查询部分,并将在事件循环线程上传递结果.因此,回调中的代码将在事件循环线程上执行. (3认同)

Tia*_*ago 50

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

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

  • 好的答案我想补充一点,I/O 发生在这个主事件循环、循环线程、请求线程之外 (2认同)

psp*_*spi 9

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

  • 计时器等待完成
  • 下一个数据块已准备好写入此文件
  • 我们正在寻找新的HTTP请求

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

(事件发生的地方有一点魔力.其中一些涉及并行运行的低级工作线程.)

在这个SQL案例中,在进行数据库查询和在回调中获取结果之间发生了很多事情(事件).在此期间,事件循环不断地将生命投入到应用程序中,并且一次推进其他请求一个小事件.因此,同时提供多个请求.

事件循环高级视图

根据:"10,000ft的事件循环 - Node.js背后的核心概念".


dhi*_*rna 5

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

c.query("Fetch Data", "Post-Processing of Data")
Run Code Online (Sandbox Code Playgroud)

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

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

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

PS:我刚开始理解节点,实际上我想写这个作为对@Philip的评论, 但由于没有足够的声誉点,所以把它写成答案.