How does node process concurrent requests?

时间:2018-07-24 10:07:38

标签: javascript node.js

I have been reading nodejs lately, trying to understand how it handles multiple concurrent requests, I know nodejs is a single threaded event loop based architecture, at a given point of time only one statement is gonna execute i.e, on main thread and blocking code/IO calls are handled by the worker threads (default is 4).

Now my question is what happens when a web server built using nodejs receives multiple requests, I know, there are a lots of Stack overflow thread that has similar questions, but didn't find a concrete answer to this.

So I am putting an example here, let's say we have following code inside a route like /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);
});

Let's assume that readImage() takes around 1 min to read that Image. If two request T1, and T2 came concurently, How nodejs is gonna process these request ?

Does it going to take first request T1, process it while queueing the request T2 (please correct me if my understanding is wrong here), if any async/blocking stuff is encountered like readImage, it then sends that to worker thread (some point later when async stuff is done it notifies the main thread and main thread starts executing the callback), move ahead by executing the next line of code. When it is done with T1 then picks T2 request ? Is it correct? or it can process T2 code in between (meaning while readImage is called, it can start processing T2)?

I would really appreciate if anyone can help me finding an answer to this question

6 个答案:

答案 0 :(得分:7)

您可能由于对事件循环的关注不够而感到困惑。显然,您对它的工作原理有所了解,但可能并不了解完整情况。

第1部分,事件循环基础

调用use方法时,在幕后发生的事情是创建了另一个线程来监听连接。

但是,当有请求进入时,由于我们与V8引擎不在同一个线程中(并且不能直接调用route函数),因此对该函数的序列化调用将附加到 shared 事件循环,以便稍后调用。 (在这种情况下,事件循环是一个不好的名字,因为它的操作更像是队列或堆栈)

在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);

setTimeout(时间为0)是一种快速简便的方法,可以将事件放入事件循环中而不会造成任何计时器复杂性,因为无论如何,它始终至少为0ms。 / em>

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

hello callback!
hello callback!
hello inner!
hello inner!

对两个callback的序列化调用都将被附加到事件循环中,然后再保证被调用。发生这种情况的原因是,在完全同步执行文件之前,无法从事件循环调用任何东西。

将文件的执行视为事件循环上的第一件事可能会有所帮助。因为从事件循环中进行的每次调用只能顺序发生,所以这成为逻辑上的结果,即在执行过程中不能发生其他事件循环。只有完成后,才能调用另一个事件循环函数。

第2部分,内部回调

相同的逻辑也适用于内部回调,并且可以用来解释为什么程序永远不会输出:

hello callback!
hello inner!
hello callback!
hello inner!

就像您期望的那样。

在文件执行结束时,将在事件循环上进行两个序列化的函数调用,这两个过程均针对callback。由于事件循环是FIFO(先进先出),因此将首先调用最先出现的setTimeout

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

这时,事件循环如下:

1 [callback] (executing)
2 [callback] (next in line)
3 [inner]    (just added by callback)

callback的返回是事件循环从其自身删除该调用的信号。现在,事件循环上剩下两件事了:对callback的另外1个调用和对inner的1个调用。

callback是该行中的下一个函数,因此将在下一个调用。该过程会重复进行。对inner的调用将附加到事件循环。 console.log打印Hello Callback!,然后从事件循环中删除对callback的调用。

这使事件循环具有另外2个功能:

1 [inner]    (next in line)
2 [inner]    (added by most recent callback)

这两个函数都不会进一步干扰事件循环,它们一个接一个地执行;第二个等待第一个返回。然后,当第二个返回时,事件循环将保留为空。结合当前没有其他线程正在运行的事实,触发了过程的结束。退出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.
}

出于解释的目的,我将假设readImage将像适当的异步函数那样立即返回。


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

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

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

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

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

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

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

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

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

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

答案 1 :(得分:3)

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

因此,当您的同步代码花费太长时间时,您将阻塞主线程,节点将无法处理其他请求,这很容易测试。

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);
});

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

答案 2 :(得分:2)

您可以通过使用fork()将readImage()函数移动到另一个文件中来简单地创建子进程。

父文件parent.js

const { fork } = require('child_process');
const forked = fork('child.js');
forked.on('message', (msg) => {
   console.log('Message from child', msg);
});

forked.send({ hello: 'world' });

子文件child.js

process.on('message', (msg) => {
  console.log('Message from parent:', msg);
});

let counter = 0;

setInterval(() => {
  process.send({ counter: counter++ });
}, 1000);

以上文章可能对您有用。

在上面的父文件中,我们派生child.js(它将使用node命令执行该文件),然后侦听message事件。每当孩子使用message时,我们都会每秒进行一次process.send事件。

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

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

答案 3 :(得分:0)

有许多文章对此进行了解释,例如this one

其长短之处在于public class SomeHandler implements InteractiveSeleniumHandler { private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); public String processDriver(WebDriver driver) { String accumulatedData = ""; try { Configuration conf = NutchConfiguration.create(); new WebDriverWait(driver, conf.getLong("libselenium.page.load.delay", 3)); WebElement more = driver.findElement(By.className("ulBlueLinks")); more.click(); LOG.error("before collecting data:"); JavascriptExecutor jsx = (JavascriptExecutor) driver; jsx.executeScript("document.body.innerHTML=document.body.innerHTML;"); accumulatedData = driver.findElement(By.tagName("body")).getAttribute("innerHTML"); } catch (Exception e) { LOG.error(StringUtils.stringifyException(e)); } return accumulatedData; } public boolean shouldProcessURL(String URL) { return true; } } 并不是真正的单线程应用程序,这是一种幻想。上面链接顶部的图对此进行了很好的解释,但是作为摘要

  • NodeJS事件循环在单个线程中运行
  • 收到请求后,会将请求移交给新线程

例如,在您的代码中,正在运行的应用程序的PID将为1。当您收到请求T1时,它将创建处理该请求的PID 2(花费1分钟)。在运行的同时,您会收到请求T2,该请求会生成PID 3,这也需要1分钟。 PID 2和3都将在任务完成后结束,但是PID 1将在事件进入时继续监听和传递事件。

总而言之,nodejs是“单线程”的,但它只是一个事件循环侦听器。当听到事件(请求)时,它将事件传递给异步执行的线程池,这意味着它不会阻止其他请求。

答案 4 :(得分:0)

V8 JS交互器(即Node)基本上是单线程的。但是,启动的进程可以是异步的,例如:“ fs.readFile”。

快递服务器运行时,将根据需要打开新进程以完成请求。因此,“ readImage”功能将被启动(通常是异步启动),这意味着它们将以任何顺序返回。但是,服务器将自动管理对哪个请求的响应。

因此,您不必管理对哪个请求进行的哪个readImage响应。

因此,基本上,T1和T2将不会同时返回,这实际上是不可能的。它们都非常依赖于文件系统来完成“读取”,并且它们可能以任何顺序完成(无法预测)。请注意,进程由OS层处理,并且本质上是多线程的(在现代计算机中)。

如果您正在寻找队列系统,则应该很容易实现/确保按照请求的确切顺序读取/返回图像。

答案 5 :(得分:-1)

由于在Marcus的先前答案中并没有真正要增加的内容,所以下面的图形说明了单线程事件循环机制:

enter image description here