以下是针对您的三个请求的一系列事件的描述:
- 三个请求被发送到 Node.js Web 服务器。
- 无论哪一个请求先于另外两个请求到达,都会触发 Web 服务器请求处理程序并开始执行。
- 另外两个请求进入 node.js 事件队列,等待轮到它们。从技术上讲,等待请求是在传入的 TCP 级别排队还是在 Node.js 内部排队(我实际上不知道),这取决于 Node.js 实现的内部结构,但出于本次讨论的目的,所有重要的是传入事件已排队,并且在第一个请求停止运行之前不会触发。
- 第一个请求处理程序将执行,直到它遇到异步操作(例如读取文件),然后在异步操作完成之前没有其他事情可做。
- 此时,异步文件 I/O 操作将启动,原始请求处理程序将返回(它已完成当时可以执行的操作)。
- 由于第一个请求(正在等待文件 I/O)现在已经返回,node.js 引擎现在可以从事件队列中拉出下一个事件并启动它。这将是到达服务器的第二个请求。它将在第一个请求时经历相同的过程,并一直运行,直到它无事可做(并且也在等待文件 I/O)。
- 当第二个请求返回到系统时(因为它正在等待文件 I/O),那么第三个请求就可以开始运行。它将遵循与前两个相同的路径。
- 当第三个请求现在也在等待 I/O 并返回到系统时,node.js 就可以自由地从事件队列中拉出下一个事件。
- 此时,所有三个请求处理程序都同时“运行”。只有一个实际上同时运行,但所有的都同时进行。
- 事件队列中的下一个事件可能是某个其他事件或某个其他请求,也可能是前面三个文件 I/O 操作之一的完成。队列中的下一个事件将开始执行。假设这是第一个请求的文件I/O操作。此时,它调用与第一个请求的文件 I/O 操作关联的完成回调,并且第一个请求开始处理文件 I/O 结果。然后,此代码将继续运行,直到完成整个请求并返回,或者直到启动其他异步操作(例如更多文件 I/O)并返回。
- 最终,第二个请求的文件 I/O 将准备就绪,并且该事件将从事件队列中提取。
- 然后,第三个请求也是如此,最终所有三个请求都会完成。
因此,即使只有一个请求实际上同时执行,多个请求也可以同时“处理中”或“进行中”。这有时被称为协作多任务处理,因为与多个本机线程的“抢占式”多任务处理不同,系统可以随时在线程之间自由切换,而是给定的 Javascript 线程运行直到返回系统,然后,只有这样,另一段 Javascript 才能开始运行。由于一段 Javascript 可以启动非阻塞异步操作,因此 Javascript 线程可以在其异步操作仍处于挂起状态时返回系统(使其他 Javascript 片段能够运行)。当这些操作完成时,它们会将一个事件发布到事件队列中,当其他 Javascript 完成并且该事件到达队列顶部时,它将运行。
单线程
这里的关键点是,给定的 Javascript 线程将一直运行,直到它返回到系统。如果在执行过程中,它启动了一些异步操作(例如文件I/O或网络),那么当这些事件完成时,它们会将一个事件放入事件队列中,当JS引擎运行完之前的任何事件时它,该事件将得到服务,并将导致回调被调用,并且该回调将轮到执行。
与多线程模型相比,这种单线程特性极大地简化了并发处理方式。在完全多线程的环境中,每个请求都启动自己的线程,因此任何希望共享的数据(即使是简单的变量)也会受到竞争条件的影响,并且必须使用互斥体进行保护,然后才能让任何人读取它。
在Javascript中,由于不存在多个请求的并发执行,因此简单的共享变量访问不需要互斥锁。根据定义,当一段 Javascript 正在读取变量时,此时没有其他 Javascript 正在运行(单线程)。
Node.js 确实使用线程
值得注意的一项技术区别是,只有 Javascript 的执行是单线程的。 Node.js 内部确实使用线程本身来完成某些事情。例如,异步文件 I/O 实际上使用本机线程。网络 I/O 实际上并不使用线程(它使用本机事件驱动的网络)。
但是,在 Node.js 内部使用线程不会直接影响 Javascript 的执行。一次仍然只有一个 Javascript 线程在执行。
竞赛条件
当启动异步操作时,正在修改的状态仍然可能存在竞争条件,但这比多线程环境中要少得多,而且识别和保护这些情况要容易得多。作为可能存在的竞争条件的示例,我有一个简单的服务器,它使用间隔计时器每 10 秒从多个温度探测器获取读数。它从所有这些温度读数中收集数据,并每小时将这些数据写入磁盘。它使用异步 I/O 将数据写入磁盘。但是,由于使用许多不同的异步文件 I/O 操作将数据写入磁盘,因此间隔计时器可能会在其中一些异步文件 I/O 操作之间触发,从而导致服务器所在的数据写入磁盘的中间要进行修改。这很糟糕,可能会导致写入不一致的数据。在一个简单的世界中,可以通过在开始将所有数据写入磁盘之前复制所有数据来避免这种情况,因此如果在将数据写入磁盘时出现新的温度读数,则副本不会受到影响,并且代码也不会受到影响。仍会将一组一致的数据写入磁盘。但是,就该服务器而言,数据可能很大,而服务器上的内存很小(它是 Raspberry Pi 服务器),因此在内存中复制所有数据是不切实际的。
因此,通过在数据写入磁盘的过程中设置一个标志,然后在数据写入磁盘完成后清除该标志来解决该问题。如果在设置此标志时触发间隔计时器,则新数据将放入单独的队列中,并且正在写入磁盘的核心数据不会被修改。当数据写入磁盘后,它会检查队列,然后将其中找到的任何温度数据添加到内存中的温度数据中。写入磁盘过程中的内容的完整性得以保留。每当遇到这种“竞争条件”并且数据因此而排队时,我的服务器都会记录一个事件。而且,你瞧,这种情况确实每隔一段时间就会发生一次,并且保护数据完整性的代码可以正常工作。