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

时间:2013-02-10 05:54:17

标签: 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吗?在我看来,由于单线程模型,没有办法从一个请求切换另一个请求。但是示例代码的标题说“除了代码之外,所有内容都是并行运行”。 (P.S我不确定我是否误解了代码,因为我从未使用过Node。)节点在等待期间如何切换到B?您能以一种简单的方式解释Node的单线程非阻塞IO模型吗? 如果你能帮助我,我将不胜感激。 :)

6 个答案:

答案 0 :(得分:363)

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

异步IO

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

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

节点的模型(续传样式和事件循环)

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

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

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

高度并发,没有并行性

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

答案 1 :(得分:199)

好吧,为了给出一些观点,让我将node.js与apache进行比较。

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

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

当在apache上收到A和B时,会创建两个处理请求的线程。每个处理查询单独,每个在服务页面之前等待查询结果。该页面仅在查询完成之前提供。查询提取是阻塞的,因为服务器在收到结果之前无法执行其余的线程。

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

  

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

实际上,节点服务器始终为您执行此操作。要创建开关,(异步行为)您将使用的大多数函数都将具有回调。

修改

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

  1. 打开与db的连接,可以异步连接本身。
  2. 连接db后,查询将传递给服务器。查询可以排队。
  3. 使用回调或事件通知主事件循环完成。
  4. 主循环执行您的回调/事件处理程序。
  5. 以类似方式处理对http服务器的传入请求。内部线程架构是这样的:

    node.js event loop

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

答案 2 :(得分:41)

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

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

答案 3 :(得分:5)

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

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

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

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

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

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

答案 4 :(得分:2)

如果你进一步阅读 - “当然,在后端,有数据库访问和流程执行的线程和进程。但是,这些没有明确地暴露给你的代码,所以你不必担心其他而不是知道I / O交互,例如与数据库或其他进程的交互,从每个请求的角度来看都是异步的,因为这些线程的结果是通过事件循环返回给你的代码的。“

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

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

答案 5 :(得分:1)

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

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

所以,再次改写它:程序永远不会(被允许)卡住,从不执行休眠呼叫。他们的职责是由内核完成(写一些东西,等待某些东西通过网络,等待时间过去)或另一个线程或进程。 - 在每个事件循环周期中,节点进程检查内核是否在对OS执行的唯一阻塞调用中完成了至少其中一项任务。当完成所有非阻塞时,就达到了这一点。

清除? : - )

我不知道Node。但c.query来自哪里?