我最近一直在阅读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将启动工作线程并立即返回。
  • 将打印第二个控制台日志。

  • 在所有这些过程中,需要注意的是,这些操作是同步进行的。在完成这些操作之前,无法启动其他事件循环调用。 readImage可能是异步的,但调用不是,异步的,工作线程的回调和用法是使它异步的原因。

    在此use回调返回之后,下一个请求可能已经完成了解析,并被添加到了事件循环中,而V8忙着做我们的控制台日志和readImage调用。

    因此,将调用下一个use回调,并重复相同的过程:登录,启动readImage线程,再次登录,然后返回。

    在此之后,读取的图像(取决于它们花费的时间)可能已经检索到了它们所需的内容,并将其回调附加到事件循环中。因此,它们将按照下一个执行的顺序执行,以先检索到其数据的顺序为准。请记住,这些操作是在单独的线程中发生的,因此,发生的事情不仅与主javascript线程平行,而且还彼此平行,所以在这里,先调用哪个无关紧要,先完成哪个无关紧要在事件循环上讨论。

    首先完成的readImage中的哪个将是第一个执行。因此,假设没有错误,我们将输出到控制台,然后将其写入词汇表范围内的相应请求的响应。

    当该发送返回时,下一个readImage回调将开始执行:控制台日志,并写入响应。

    此时,两个readImage线程均已终止,并且事件循环为空,但是保存服务器端口绑定(bind)的线程将使进程保持 Activity 状态,等待其他事件添加到事件循环中,然后循环继续进行。

    我希望这可以帮助您了解所提供示例的异步本质背后的机制

    09-10 11:15
    查看更多