节点如何处理并发请求?

Sar*_*roj 22 javascript node.js

我最近一直在阅读nodejs,试图了解它如何处理多个并发请求,我知道nodejs是一个基于单线程事件循环的架构,在给定的时间点只有一个语句将被执行,即在主线程和阻塞代码/ IO调用由工作线程处理(默认为4).

现在我的问题是当使用nodejs构建的Web服务器收到多个请求时会发生什么,我知道,有很多Stack溢出线程有类似的问题,但没有找到具体的答案.

所以我在这里举一个例子,假设我们在/ 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);
});
Run Code Online (Sandbox Code Playgroud)

我们假设readImage()需要大约1分钟来读取该Image.如果两个请求T1和T2来得很清楚,nodejs将如何处理这些请求?

是否需要第一个请求T1,在排队请求T2时处理它(如果我的理解错误,请纠正我),如果遇到像readImage那样的任何异步/阻塞内容,它会将其发送给工作线程(稍后某点)当异步填充完成时它通知主线程并且主线程开始执行回调),通过执​​行下一行代码继续前进.当完成T1然后选择T2请求?这是对的吗?或者它可以在其间处理T2代码(意味着在调用readImage时,它可以开始处理T2)?

如果有人能帮我找到这个问题的答案,我真的很感激

Mar*_*lin 9

你的困惑可能来自于没有足够关注事件循环.显然你已经知道这是如何工作的,但也许不是全貌.

第1部分,事件循环基础知识

当您调用该use方法时,幕后发生的是创建另一个线程来监听连接.

但是,当请求进入时,因为我们在与V8引擎不同的线程中(并且不能直接调用路由函数),对函数的序列化调用将附加到共享事件循环中,以便稍后调用它.(事件循环在此上下文中是一个很差的名称,因为它更像是队列或堆栈)

在js文件的末尾,V8将检查事件循环中是否有任何正在运行的磁带或消息.如果没有,它将退出0(这就是服务器代码保持进程运行的原因).因此要理解的第一个Timing细微差别是在到达js文件的同步结束之前不会处理任何请求.

如果在进程启动时附加了事件循环,则事件循环上的每个函数调用将完全同步地逐个处理.

为简单起见,让我将您的示例分解为更具表现力的内容.

function callback() {
    setTimeout(function inner() {
        console.log('hello inner!');
    }, 0); // †
    console.log('hello callback!');
}

setTimeout(callback, 0);
setTimeout(callback, 0);
Run Code Online (Sandbox Code Playgroud)

setTimeout时间为0,是一种快速简便的方法,可以在事件循环中放置一些没有任何计时器复杂的东西,因为无论如何,它始终至少为0ms.

在此示例中,输出将始终为:

hello callback!
hello callback!
hello inner!
hello inner!
Run Code Online (Sandbox Code Playgroud)

两个序列化调用都会在调用callback其中任何一个之前附加到事件循环中,并保证.发生这种情况是因为直到完全同步执行文件之后才能从事件循环中调用任何内容.

考虑文件的执行情况会很有帮助,这是事件循环中的第一件事.因为事件循环中的每次调用只能串行发生,所以它成为一个逻辑结果,在执行过程中不会发生其他事件循环调用; 只有当它完成时,才能调用另一个事件循环函数.

第2部分,内部回调

同样的逻辑同样适用于内部回调,可用于解释程序永远不会输出的原因:

hello callback!
hello inner!
hello callback!
hello inner!
Run Code Online (Sandbox Code Playgroud)

就像你想象的那样.

在文件执行结束时,两个序列化函数调用将在事件循环中进行,两者都是callback.由于事件循环是FIFO(先进先出),所以setTimeout首先调用它.

首先要做的callback是执行另一个setTimeout.和以前一样,这会将一个序列化的调用追加到事件循环,这次是inner函数.setTimeout立即返回,执行将继续执行到第一个console.log.

此时,事件循环如下所示:

1 [callback] (executing)
2 [callback] (next in line)
3 [inner]    (just added by callback)
Run Code Online (Sandbox Code Playgroud)

返回callback是事件循环从其自身中删除该调用的信号.这在事件循环上留下了两件事:1个调用callback,1个调用inner.

callback是下一个函数,所以接下来会调用它.这个过程重演.调用将inner附加到事件循环.一个console.log版画Hello Callback!和我们完成除去此调用callback从事件循环.

这为事件循环留下了另外两个函数:

1 [inner]    (next in line)
2 [inner]    (added by most recent callback)
Run Code Online (Sandbox Code Playgroud)

这些函数都没有进一步混乱事件循环,它们一个接一个地执行; 第二个等待第一个回归.然后当第二个返回时,事件循环保持为空.这结合了当前没有其他线程正在运行的事实,触发了该过程的结束.退出0.

第3部分,与原始示例有关

在您的示例中发生的第一件事是,在进程内创建一个线程,该线程将创建绑定到特定端口的服务器.注意,这是在预编译的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.
}
Run Code Online (Sandbox Code Playgroud)

出于解释的目的,我将在假设readImage将立即返回的情况下运行,因为正确的异步函数应该.


一旦use开始回调执行,将发生以下情况:

  1. 将打印第一个控制台日志.
  2. readImage将启动一个工作线程并立即返回.
  3. 将打印第二个控制台日志.

在所有这一切中,重要的是要注意,这些操作是同步进行的; 在这些事件完成之前,没有其他事件循环调用可以启动.readImage可能是异步的,但调用它不是,工作线程的回调和使用是使它异步的原因.

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

所以use调用下一个回调,并重复相同的过程:log,启动readImage线程,再次登录,返回.

在此之后,读取图像(取决于它们花费多长时间)可能已经检索了它们所需的内容并将它们的回调附加到事件循环中.因此,它们将按照先检索其数据的顺序执行.记住,这些操作是在不同的线程中发生的,所以这不仅与主javascript线程并行,而且彼此平行,所以在这里,首先调用哪一个并不重要,哪个先完成,然后得到了dibs在事件循环上.

无论哪个readImage首先完成,都将是第一个执行的.所以,假设没有错误,我们将打印到控制台,然后写入相应请求的响应,保存在词法范围内.

当该发送返回时,下一个readImage回调将开始执行:console log,并写入响应.

此时,两个readImage线程都已死亡,并且事件循环为空,但保存服务器端口绑定的线程使进程保持活动状态,等待其他内容添加到事件循环,并继续循环.

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

  • 如果您想详细了解线程在幕后如何工作,我至少会提到 Node 使用 [libuv](https://github.com/libuv/libuv) 来实现其异步 API。 (2认同)

小智 5

对于每一个传入的请求,节点都会一一处理。这意味着必须有顺序,就像队列一样,先到先得。当 node 开始处理请求时,所有同步代码都会执行,异步将传递给工作线程,因此 node 可以开始处理下一个请求。当异步部分完成后,它将返回主线程并继续运行。

所以当你的同步代码时间过长时,你阻塞了主线程,节点将无法处理其他请求,这很容易测试。

app.use('/index', function(req, res, next) {
    // synchronous part
    console.log("hello index routes was invoked");
    var sum = 0;
    // useless heavy task to keep running and block the main thread
    for (var i = 0; i < 100000000000000000; i++) {
        sum += i;
    }
    // asynchronous part, pass to work thread
    readImage("path", function(err, content) {
        // when work thread finishes, add this to the end of the event loop and wait to be processed by main thread
        status = "Success";
        if(err) {
            console.log("err :", err);
            status = "Error"
        }
        else {
            console.log("Image read");
        }
        return res.send({ status: status });
    });
    // continue synchronous part at the same time.
    var a = 4, b = 5;
    console.log("sum =", a + b);
});
Run Code Online (Sandbox Code Playgroud)

在完成所有同步部分之前,节点不会开始处理下一个请求。所以人们说不要阻塞主线程。


mur*_*ain 2

您可以通过使用 fork() 将 readImage() 函数转移到不同的文件中来简单地创建子进程。

\n\n

父文件,parent.js

\n\n
const { fork } = require('child_process');\nconst forked = fork('child.js');\nforked.on('message', (msg) => {\n   console.log('Message from child', msg);\n});\n\nforked.send({ hello: 'world' });\n
Run Code Online (Sandbox Code Playgroud)\n\n

子文件,child.js

\n\n
process.on('message', (msg) => {\n  console.log('Message from parent:', msg);\n});\n\nlet counter = 0;\n\nsetInterval(() => {\n  process.send({ counter: counter++ });\n}, 1000);\n
Run Code Online (Sandbox Code Playgroud)\n\n

以上文章可能对您有用。

\n\n

在上面的父文件中,我们分叉child.js(这将使用节点命令执行文件),然后监听事件messagemessage每当孩子使用 时,就会发出该事件process.send,我们\xe2\x80\x99每秒都会执行此操作。

\n\n

要将消息从父级传递给子级,我们可以send在分叉对象本身上执行该函数,然后在子脚本中,我们可以监听message全局process对象上的事件。

\n\n

当执行parent.js上面的文件时,它\xe2\x80\x99将首先发送{ hello: 'world' }要由fork子进程打印的对象,然后fork子进程将每秒发送一个递增的计数器值以由父进程打印。

\n