单线程非阻塞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等待。 程序在I / O等待中“卡住”,不能执行呈现网页的代码。

程序在等待期间是否会切换到请求B?

在我看来,因为它是单线程模型,所以没有办法从一个请求切换到另一个。 但是示例代码的标题说“除了代码之外所有东西都是并行运行的”

(PS我不知道,如果我误解了代码或没有,因为我从来没有使用Node

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

如果你能帮助我,我将不胜感激。 🙂

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)的签名function (... parameters ..., callback)并且需要给出一个回调function (... parameters ..., callback)当请求的操作完成时会被调用(请记住,大部分时间都是等待的为操作系统发出信号完成时间 – 可以用来做其他工作)。 Javascript对闭包的支持允许您使用在回调主体内部的外部(调用)函数中定义的变量 – 这允许在节点运行时独立调用的不同函数之间保持状态。 另请参见延续传递风格 。

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

您可以将节点的事件循环看作与内核的调度程序有点类似 :一旦完成了挂起的IO,内核就会安排执行被阻塞的线程,而当相应的事件发生时,节点将安排回调。

高度并发,不平行

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

那么,给一些观点,让我比较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. 打开连接到数据库,连接本身可以异步。
  2. 一旦数据库连接,查询被传递到服务器。 查询可以排队。
  3. 主事件循环通过回调或事件得到完成通知。
  4. 主循环执行您的回调/事件处理程序。

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

node.js事件循环

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

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

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

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

  • 定时器等待完成
  • 下一块数据已经准备好写入这个文件
  • 新的HTTP请求来了我们的方式

所有这些都以单线程运行,并没有JavaScript代码并行执行。 只要这些事件处理程序很小,而且还要等待更多的事件,那么所有事情都可以很好地工作。 这允许多个请求被一个Node.js进程同时处理。

(事件发生的地方有一些魔术,其中一些涉及低级别的工作线程并行运行。)

在这个SQL例子中, 在进行数据库查询和在回调中获取结果之间发生了很多事情(事件) 。 在此期间,事件循环一直在应用程序中增加生命,并一次性推进其他请求中的一个小事件。 因此多个请求正在同时服务。

事件循环高级别视图

根据: “事件循环从10,000ft – 核心概念背后Node.js” 。

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

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

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

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

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

PS:我刚刚开始理解节点,其实我想写这个评论给@Philip,但是因为没有足够的声望点,所以把它写成了答案。

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

关于“除了你的代码外,所有的东西都是并行运行的” – 你的代码是同步执行的,每当你调用一个异步操作,例如等待IO时,事件循环就会处理所有事件并调用回调函数。 这不是你必须考虑的事情。

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

好的,大部分事情应该是清楚的… 棘手的部分是SQL :如果它不是在另一个线程或进程中完全运行,SQL执行必须被分解成单独的步骤 SQL处理器是为异步执行而制作的!),其中执行非阻塞操作,实际上可以将阻塞操作(例如睡眠)传输到内核(作为警报中断/事件),并放到事件列表中主循环。

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

因此,再次重申:程序永远不会(被允许)被卡住,睡眠呼叫永远不会执行。 他们的职责是由内核完成的(写一些东西,等待网络上的东西,等待时间过去)或者其他线程或进程。 – 节点进程在每个事件循环周期中检查是否至少有一个职责是由内核在OS的唯一阻塞调用中完成的。 这一点是达成的,当一切都非阻塞完成。

明确? 🙂

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