javascript - 节点如何处理并发请求?
问题描述
我最近一直在阅读 nodejs,试图了解它如何处理多个并发请求。我知道 NodeJs 是一个基于单线程事件循环的架构,在给定的时间点,只有一个语句将被执行,即在主线程上,阻塞代码/IO 调用由工作线程处理(默认为 4 )。
现在我的问题是,当使用 NodeJs 构建的 Web 服务器收到多个请求时会发生什么?我知道这里有很多类似的问题,但还没有找到我的问题的具体答案。
举个例子,假设我们在/index这样的路由中有以下代码:
app.use('/index', function(req, res, next) {
console.log("hello index routes was invoked");
readImage("path", function(err, content) {
status = "Success";
if(err) {
console.log("err :", err);
status = "Error"
}
else {
console.log("Image read");
}
return res.send({ status: status });
});
var a = 4, b = 5;
console.log("sum =", a + b);
});
假设该readImage()
函数需要大约 1 分钟来读取该图像。
如果两个请求 T1 和 T2 同时进来,NodeJs 将如何处理这些请求?
它会先处理请求 T1,然后在排队请求 T2 的同时处理它吗?我假设如果遇到任何异步/阻塞的东西,例如readImage
,它然后将其发送到工作线程(然后稍后当异步东西完成时,线程通知主线程并且主线程开始执行回调?),然后继续执行下一行代码?
当 T1 完成后,它会处理 T2 请求吗?那是对的吗?或者它可以在两者之间处理 T2(意味着当代码readImage
运行时,它可以开始处理 T2)?
那正确吗?
解决方案
您的困惑可能来自对事件循环的关注不够。显然,您知道这是如何工作的,但也许您还没有全貌。
第 1 部分,事件循环基础
当您调用该use
方法时,会在幕后创建另一个线程来侦听连接。
然而,当一个请求进来时,因为我们在与 V8 引擎不同的线程中(并且不能直接调用路由函数),对该函数的序列化调用被附加到共享事件循环中,以便稍后调用. (在这种情况下,'事件循环'是一个糟糕的名字,因为它更像是一个队列或堆栈)
在 JavaScript 文件的末尾,V8 引擎将检查事件循环中是否有任何正在运行的广告或消息。如果没有,它将以代码 0 退出(这就是服务器代码保持进程运行的原因)。所以要理解的第一个 Timing 细微差别是在到达 JavaScript 文件的同步结束之前不会处理任何请求。
如果在进程启动时附加了事件循环,则事件循环上的每个函数调用将被一个一个地同步处理。
为简单起见,让我将您的示例分解为更具表现力的示例。
function callback() {
setTimeout(function inner() {
console.log('hello inner!');
}, 0); // †
console.log('hello callback!');
}
setTimeout(callback, 0);
setTimeout(callback, 0);
†setTimeout
时间为 0,是一种快速简便的方法,可以在没有任何计时器复杂性的情况下将某些内容放在事件循环中,因为无论如何,它始终至少为 0 毫秒。
在此示例中,输出将始终为:
hello callback!
hello callback!
hello inner!
hello inner!
在调用其中任何一个之前,这两个序列化调用callback
都附加到事件循环中。这是有保证的。发生这种情况是因为在文件完全同步执行之前,无法从事件循环中调用任何内容。
将文件的执行视为事件循环中的第一件事可能会有所帮助。因为事件循环中的每个调用只能连续发生,所以它成为一个合乎逻辑的结果,在其执行期间不能发生其他事件循环调用;只有上一次调用完成后,才能调用下一个事件循环函数。
第 2 部分,内部回调
同样的逻辑也适用于内部回调,可以用来解释为什么程序永远不会输出:
hello callback!
hello inner!
hello callback!
hello inner!
就像你可能期望的那样。
在文件执行结束时,两个序列化函数调用将在事件循环中,都是 for callback
。由于事件循环是一个FIFO(先进先出),因此setTimeout
先来的将首先被调用。
第一件事callback
是执行另一个setTimeout
. 和以前一样,这将附加一个序列化调用,这次是inner
函数,事件循环。setTimeout
立即返回,执行将继续执行第一个console.log
.
此时,事件循环如下所示:
1 [callback] (executing)
2 [callback] (next in line)
3 [inner] (just added by callback)
的返回callback
是事件循环从自身中删除该调用的信号。现在,这在事件循环中留下了 2 件事:1 次对 的调用callback
和 1 次对 的调用inner
。
Nowcallback
是下一个函数,所以接下来会调用它。该过程不断重复。调用inner
附加到事件循环。console.log
打印出来,我们通过从事件循环Hello Callback!
中删除这个调用来完成。callback
这为事件循环留下了另外 2 个功能:
1 [inner] (next in line)
2 [inner] (added by most recent callback)
这些函数都不会进一步干扰事件循环。他们一个接一个地执行,第二个等待第一个返回。然后当第二个返回时,事件循环为空。这一事实与当前没有其他线程在运行的事实相结合,触发了进程的结束,该进程以返回码 0 退出。
第 3 部分,与原始示例相关
在您的示例中发生的第一件事是在进程中创建一个线程,该线程将创建一个绑定到特定端口的服务器。请注意,这发生在预编译的 C++ 代码中,而不是 JavaScript,并且不是一个单独的进程,它是同一进程中的一个线程。请参阅:C++ 线程教程。
所以现在,只要有请求进来,你的原始代码的执行就不会受到干扰。相反,传入的连接请求将被打开、保留并附加到事件循环中。
该use
功能是捕获传入请求事件的网关。它是一个抽象层,但为了简单起见,将use
函数像setTimeout
. 除了等待一定时间之外,它会在传入的 http 请求时将回调附加到事件循环。
所以,让我们假设有两个请求进入服务器:T1 和 T2。在您的问题中,您说它们同时出现,因为这在技术上是不可能的,我将假设它们一个接一个,它们之间的时间可以忽略不计。
无论哪个请求先进来,都将首先由之前的辅助线程处理。一旦该连接被打开,它就会被附加到事件循环中,我们继续下一个请求,并重复。
在第一个请求被添加到事件循环后的任何时候,V8 都可以开始执行use
回调。
快速搁置一下readImage
由于不清楚是否readImage
来自特定库、您编写的内容或其他内容,因此无法准确判断在这种情况下它将做什么。但是只有两种可能性,所以它们是:
- 它是完全同步的,从不使用备用线程或事件循环
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.
}
- 它完全是异步的,并利用了 fs 的异步回调。
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
将启动一个工作线程并立即返回。- 将打印第二个控制台日志。
在所有这些过程中,需要注意的是,这些操作是同步发生的;在这些完成之前,无法启动其他事件循环调用。readImage
可能是异步的,但调用它不是,工作线程的回调和使用是使其异步的原因。
在这个use
回调返回之后,下一个请求可能已经完成解析并被添加到事件循环中,而 V8 正忙于处理我们的控制台日志和 readImage 调用。
因此use
调用下一个回调,并重复相同的过程:记录,启动 readImage 线程,再次记录,返回。
在此之后,readImage 函数(取决于它们需要多长时间)可能已经检索到它们需要的内容并将它们的回调附加到事件循环中。因此,它们将在接下来执行,以先检索数据的顺序执行。请记住,这些操作是在不同的线程中发生的,所以它们不仅与主 javascript 线程并行发生,而且彼此并行,所以在这里,先调用哪个并不重要,重要的是哪个先完成,并在事件循环中获得“dibs”。
谁先readImage
完成,谁就先执行。因此,假设没有发生错误,我们将打印到控制台,然后写入对应请求的响应,并保存在词法范围内。
当该发送返回时,下一个 readImage 回调将开始执行:控制台日志,并写入响应。
此时,两个 readImage 线程都已死亡,并且事件循环为空,但持有服务器端口绑定的线程正在保持进程处于活动状态,等待将其他内容添加到事件循环中,然后循环继续。
我希望这可以帮助您了解您提供的示例的异步性质背后的机制。
推荐阅读
- google-apps-script - 无法在任何文件上授权任何外部 Google Apps 脚本范围
- pdfbox - 尽管密码错误,pdfbox加密文件仍会打开
- javascript - Nest.js 无法解析依赖,找不到我的错误
- javascript - Local lighthouse has much different score than web.dev lighthouse
- python - 对列名满足字符串匹配条件的所有列的行值求和
- android - 如何在不直接引用数组名称的情况下从 json 数组中获取数据
- python - How to set url in Django?
- c# - 如何统一指挥后退
- javascript - 最大化表格高度,并在 JavaScript/React.js 中包含滚动条
- amazon-web-services - 默认情况下是 Cognito 高可用服务吗?