我最近一直在阅读nodejs,试图了解它如何处理多个并发请求,我知道nodejs是基于单线程事件循环的体系结构,在给定的时间点将只执行一条语句,即在主线程上执行并阻塞代码/IO调用由工作线程处理(默认为4)。
现在我的问题是,当使用nodejs构建的Web服务器收到多个请求时,会发生什么情况,我知道,很多Stack溢出线程都存在类似的问题,但没有找到具体的答案。
所以我在这里举一个例子,假设我们在/index 之类的路由内有以下代码。
假设readImage()大约需要1分钟才能读取该Image。
如果两个请求T1和T2同时出现,那么nodejs将如何处理这些请求?
它将接收第一个请求T1,在对请求T2进行排队的同时对其进行处理(如果我的理解是错误的,请纠正我),如果遇到任何异步/阻塞的东西(如readImage),则将其发送到工作线程(某些线程)稍后,当异步内容完成后,它会通知主线程,并且主线程开始执行回调),通过执行下一行代码继续前进。
T1完成后,再选择T2请求?这是正确的吗?还是可以在两者之间处理T2代码(意味着在调用readImage的同时可以开始处理T2)?
如果有人能帮助我找到这个问题的答案,我将不胜感激
最佳答案
您可能由于对事件循环的关注不够而感到困惑。显然,您对这是如何工作的有一个想法,但可能不是全部。
第1部分,事件循环基础
当您调用use
方法时,在幕后发生的事情是创建了另一个线程来监听连接。
但是,当有请求进入时,由于我们与V8引擎不在同一个线程中(并且不能直接调用route函数),因此对该函数的序列化调用将附加到共享事件循环中,以便稍后调用。 (在这种情况下,事件循环是一个不好的名字,因为它的操作更像是队列或堆栈)
在js文件的末尾,V8将检查事件循环中是否有任何正在运行的广告或消息。如果不存在,它将退出0(这就是服务器代码保持进程运行的原因)。因此,首先要了解的时间上的细微差别是,直到达到js文件的同步末尾,才会处理任何请求。
如果在进程启动时将事件循环附加到该事件循环,则将完全同步地逐个处理事件循环上的每个函数调用。
为简单起见,让我将您的示例分解为更具表现力的示例。
function callback() {
setTimeout(function inner() {
console.log('hello inner!');
}, 0); // †
console.log('hello callback!');
}
setTimeout(callback, 0);
setTimeout(callback, 0);
†时间为0的
setTimeout
是一种快速简便的方法,可以将事件放入事件循环中而不会造成任何计时器复杂性,因为无论如何,它始终至少为0ms。在此示例中,输出将始终为:
hello callback!
hello callback!
hello inner!
hello inner!
序列化的对
callback
的两个调用都将被附加到事件循环中,然后再调用其中的任何一个,确保。发生这种情况的原因是,在完全同步执行文件之前,无法从事件循环调用任何内容。将文件的执行视为事件循环中的第一件事,可能会有所帮助。因为从事件循环中进行的每次调用只能顺序发生,所以这成为逻辑上的结果,即在执行过程中不能再发生其他事件循环。只有完成后,才能调用另一个事件循环功能。
第2部分,内部回调
相同的逻辑也适用于内部回调,并且可以用来解释该程序永远不会输出的原因:
hello callback!
hello inner!
hello callback!
hello inner!
就像您可能期望的那样。
在文件执行结束时,将在事件循环上进行2个序列化的函数调用,均针对
callback
。由于事件循环是FIFO(先进先出),因此将首先调用最先出现的setTimeout
。callback
做的第一件事是执行另一个setTimeout
。像以前一样,这将向事件循环追加一次序列化调用,这一次是inner
函数。 setTimeout
立即返回,执行将继续到第一个console.log
。此时,事件循环如下所示:
1 [callback] (executing)
2 [callback] (next in line)
3 [inner] (just added by callback)
callback
的返回是事件循环从其自身删除该调用的信号。现在,在事件循环上剩下两件事了:还有1个对callback
的调用和1个对inner
的调用。callback
是该行中的下一个函数,因此将在下一步调用它。该过程会重复进行。对inner
的调用将附加到事件循环中。 console.log
会打印Hello Callback!
,我们首先从事件循环中删除对callback
的调用。这使事件循环具有另外2个功能:
1 [inner] (next in line)
2 [inner] (added by most recent callback)
这些函数都不会进一步干扰事件循环。他们一个接一个地执行,第二个等待第一个的返回。然后,当第二个返回时,事件循环将保留为空。这与当前没有其他线程正在运行的事实相结合,将触发流程结束,并以0的返回码退出。
第三部分,与原始示例有关
在您的示例中发生的第一件事是,在进程内创建了一个线程,该线程将创建绑定(bind)到特定端口的服务器。注意,这是在预编译的C++中发生的,不是javascript,也不是一个单独的进程,它是同一进程中的一个线程。请参阅:C++ Thread Tutorial
因此,现在,无论何时有请求出现,原始代码的执行都不会受到干扰。相反,传入的连接请求将被打开,保留并附加到事件循环中。
use
函数是捕获传入请求事件的网关。它是一个抽象层,但是为了简单起见,它像use
一样有助于想到setTimeout
函数。除非等待指定的时间,否则它将在传入的HTTP请求后将回调附加到事件循环中。 因此,假设有两个请求进入服务器:T1和T2。在您的问题中,您说它们同时出现,因为从技术上讲这是不可能的,所以我假设它们是一个接一个的,它们之间的时间可以忽略不计。
无论哪个请求先出现,都会由较早的辅助线程首先处理。打开该连接后,将其附加到事件循环,然后继续进行下一个请求,然后重复。
在将第一个请求添加到事件循环后的任何时候,V8都可以开始执行
use
回调。快速阅读readImage
由于尚不清楚
readImage
是否来自特定的库(您编写的内容还是其他内容),因此无法确切说明在这种情况下它将执行的操作。虽然只有两种可能性,所以它们是:// in this example definition of readImage, its entirely
// synchronous, never using an alternate thread or the
// event loop
function readImage (path, callback) {
let image = fs.readFileSync(path);
callback(null, image);
// a definition like this will force the callback to
// fully return before readImage returns. This means
// means readImage will block any subsequent calls.
}
// in this alternate example definition its entirely
// asynchronous, and take advantage of fs' async
// callback.
function readImage (path, callback) {
fs.readFile(path, (err, data) => {
callback(err, data);
});
// a definition like this will force the readImage
// to immediately return, and allow exectution
// to continue.
}
为了便于说明,我将假设readImage将像适当的异步函数那样立即返回。
一旦开始执行
use
回调,将发生以下情况:在所有这些过程中,需要注意的是,这些操作是同步进行的。在完成这些操作之前,无法启动其他事件循环调用。 readImage可能是异步的,但调用不是,异步的,工作线程的回调和用法是使它异步的原因。
在此
use
回调返回之后,下一个请求可能已经完成了解析,并被添加到了事件循环中,而V8忙着做我们的控制台日志和readImage调用。因此,将调用下一个
use
回调,并重复相同的过程:登录,启动readImage线程,再次登录,然后返回。在此之后,读取的图像(取决于它们花费的时间)可能已经检索到了它们所需的内容,并将其回调附加到事件循环中。因此,它们将按照下一个执行的顺序执行,以先检索到其数据的顺序为准。请记住,这些操作是在单独的线程中发生的,因此,发生的事情不仅与主javascript线程平行,而且还彼此平行,所以在这里,先调用哪个无关紧要,先完成哪个无关紧要在事件循环上讨论。
首先完成的readImage中的哪个将是第一个执行。因此,假设没有错误,我们将输出到控制台,然后将其写入词汇表范围内的相应请求的响应。
当该发送返回时,下一个readImage回调将开始执行:控制台日志,并写入响应。
此时,两个readImage线程均已终止,并且事件循环为空,但是保存服务器端口绑定(bind)的线程将使进程保持 Activity 状态,等待其他事件添加到事件循环中,然后循环继续进行。
我希望这可以帮助您了解所提供示例的异步本质背后的机制