Node.js 有多个并发请求的服务器,它是如何工作的?

Node.js server with multiple concurrent requests, how does it work?

我知道node.js是单线程、异步、非阻塞i/o。我已经阅读了很多相关内容。例如 PHP 每个请求使用一个线程,但节点只对所有请求使用一个线程,就像那样。

假设有3个请求a,b,c同时到达node.js服务器。其中三个请求需要大阻塞操作,例如它们都想读取同一个大文件。

那么请求是如何排队的,阻塞操作是按什么顺序执行的,响应是按什么顺序发送的?当然使用多少线程?

请告诉我三个请求从请求到响应的顺序。

以下是对您的三个请求的一系列事件的描述:

  1. 三个请求被发送到 node.js 网络服务器。
  2. 无论哪个请求在其他两个请求之前到达,都会触发 Web 服务器请求处理程序并开始执行。
  3. 另外两个请求进入 node.js 事件队列,等待轮到他们。技术上取决于 node.js 实现的内部,等待请求是在传入的 TCP 级别排队还是在 node.js 内部排队(我实际上不知道),但为了在本次讨论中,重要的是传入事件已排队,并且在第一个请求停止之前不会触发 运行ning.
  4. 第一个请求处理程序将执行直到它遇到异步操作(例如读取文件),然后在异步操作完成之前无事可做。
  5. 此时,异步文件 I/O 操作被启动,原始请求处理程序 returns(它已经完成了它当时可以做的事情)。
  6. 由于第一个请求(正在等待文件 I/O)现在已经 returned,node.js 引擎现在可以从事件队列中拉出下一个事件并且启动它。这将是到达服务器的第二个请求。它将在第一次请求时经历相同的过程,并且将 运行 直到它无事可做(并且还在等待文件 I/O)。
  7. 当第二个请求 returns 返回系统时(因为它正在等待文件 I/O),那么第三个请求可以开始 运行ning。它将遵循与前两个相同的路径。
  8. 当第三个请求现在也在等待 I/O 和 return 返回系统时,node.js 然后可以自由地将下一个事件从事件队列中拉出。
  9. 此时,所有三个请求处理程序同时 "in flight"。实际上一次只有一个 运行,但所有都在同时进行。
  10. 事件队列中的下一个事件可能是其他事件或其他请求,也可能是前三个文件 I/O 操作之一的完成。队列中的下一个事件将开始执行。假设是第一个请求的文件 I/O 操作。此时,它调用与第一个请求的文件 I/O 操作关联的完成回调,并且第一个请求开始处理文件 I/O 结果。然后这段代码将继续 运行 直到它完成整个请求和 returns 或者直到它开始一些其他异步操作(比如更多文件 I/O)和 returns .
  11. 最终,第二个请求的文件 I/O 将准备就绪,该事件将从事件队列中提取。
  12. 然后,第三个请求也一样,最终三个请求都会完成。

因此,即使只有一个请求实际上同时在执行,多个请求也可以同时 "in process" 或 "in flight"。这有时被称为协作多任务处理,而不是 "pre-emptive" 具有多个本地线程的多任务处理,其中系统可以随时在线程之间自由切换,给定线程 Javascript 运行s直到它 return 回到系统,然后,只有这样,另一块 Javascript 才能开始 运行ning。因为一块Javascript可以发起非阻塞异步操作,所以Javascript的线程可以return回系统(启用其他的Javascript到运行) 而它的异步操作仍然悬而未决。当这些操作完成时,它们将 post 一个事件添加到事件队列中,当其他 Javascript 完成并且该事件到达队列顶部时,它将 运行.

单线程

这里的关键点是 Javascript 的给定线程将 运行 直到它 return 返回系统。如果,在执行的过程中,启动了一些异步操作(比如文件I/O或者网络),那么当这些事件执行完后,会在事件队列中放入一个事件,当JS引擎执行完运行在它之前发生任何事件,该事件将得到服务并导致回调被调用,该回调将轮到它执行。

与多线程模型相比,这种单线程性质极大地简化了并发处理的方式。在一个完全多线程的环境中,每个请求都启动自己的线程,然后是任何希望共享的数据,即使是一个简单的变量也会受到竞争条件的影响,并且必须用互斥锁保护,然后任何人才能读取它。

在Javascript中因为没有并发执行多个请求,所以简单的共享变量访问不需要mutex。根据定义,此时 Javascript 的一部分正在读取变量,此时没有其他 Javascript 运行ning(单线程)。

Node.js 是否使用线程

一个值得注意的技术区别是只有 Javascript 的执行是单线程的。 node.js 内部确实在某些事情上使用线程本身。例如,异步文件 I/O 实际上使用了本机线程。网络 I/O 实际上不使用线程(它使用本机事件驱动网络)。

但是,node.js 内部线程的这种使用不会直接影响 Javascript 的执行。一次仍然只有一个 Javascript 线程在执行。

竞争条件

当启动异步操作时,仍然可能存在正在修改状态的竞争条件,但这比在多线程环境中要少得多,而且更容易识别并保护这些案件。作为可能存在的竞争条件的示例,我有一个简单的服务器,它使用间隔计时器每 10 秒从多个温度探测器获取读数。它从所有这些温度读数中收集数据,并且每小时将这些数据写入磁盘。它使用 async I/O 将数据写入磁盘。但是,由于使用了许多不同的异步文件 I/O 操作将数据写入磁盘,因此间隔计时器可能会在其中一些异步文件 I/O 操作之间触发,从而导致数据服务器正在写入要修改的磁盘。这很糟糕,会导致写入不一致的数据。在一个简单的世界中,可以通过在开始将数据写入磁盘之前制作所有数据的副本来避免这种情况,因此如果在将数据写入磁盘时出现新的温度读数,副本将不会受到影响并且代码仍会将一组一致的数据写入磁盘。但是,在这个服务器的情况下,数据可能很大而服务器上的内存很小(它是一个 Raspberry Pi 服务器)所以在内存中复制所有数据是不切实际的。

所以,通过在数据写入磁盘的过程中设置标志,然后在数据写入磁盘完成时清除标志来解决问题。如果在设置此标志时触发间隔计时器,则新数据将放入单独的队列中,并且不会修改正在写入磁盘的核心数据。数据写入磁盘后,它会检查队列,然后将找到的任何温度数据添加到内存中的温度数据中。写入磁盘过程中的内容的完整性得以保留。只要这个 "race condition" 被命中并且数据因此排队,我的服务器就会记录一个事件。而且,你瞧,它确实每隔一段时间就会发生一次,并且保持数据完整性的代码有效。