Как узел обрабатывает одновременные запросы?

Я читал nodejs в последнее время, пытаясь понять, как он обрабатывает несколько параллельных запросов. Я знаю, что nodejs - это однопоточная архитектура, основанная на циклах событий, в данный момент времени будет выполняться только один оператор, т.е. На основной поток и блокирующий код/Вызовы IO обрабатываются рабочими потоками (по умолчанию 4).

Теперь мой вопрос заключается в том, что происходит, когда веб-сервер, построенный с использованием nodejs, получает несколько запросов, я знаю, существует множество потоков, которые имеют похожие вопросы, но не нашли конкретного ответа на этот вопрос.

Поэтому я приводил пример здесь, допустим, у нас есть код внутри маршрута, например /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

Ваша путаница может возникнуть из-за недостаточной фокусировки на контуре событий. ясно, что у вас есть представление о том, как это работает, но, возможно, не полная картина.

Часть 1, Основы Event Loop

Когда вы вызываете метод use, что происходит за кулисами, создается другой поток для прослушивания соединений.

Однако, когда приходит запрос, потому что мы находимся в другом потоке, чем двигатель V8 (и не можем напрямую ссылаться на функцию маршрута), сериализованный вызов функции добавляется в общий цикл событий, поскольку он будет называться позже, (цикл событий является бедным именем в этом контексте, поскольку он работает скорее как очередь или стек)

в конце js файла, V8 проверяет, есть ли какие-либо запущенные теги или сообщения в цикле событий. Если их нет, он выйдет из 0 (поэтому серверный код продолжает работать). Таким образом, первый момент времени для понимания заключается в том, что никакой запрос не будет обработан до тех пор, пока не будет достигнут синхронный конец js файла.

Если цикл событий был добавлен во время запуска процесса, каждый вызов функции в цикле событий будет обрабатываться один за другим, в полном объеме, синхронно.

Для простоты позвольте мне разложить ваш пример на нечто более выразительное.

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!

Как и следовало ожидать.

По завершении выполнения файла в цикле событий будут выполняться 2 последовательных вызова функций, как для callback. Поскольку цикл Event является 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.

callback - следующая функция в строке, поэтому она будет вызываться следующим образом. Процесс повторяется. Вызов inner элемента добавляется к циклу событий. console.log печатает Hello Callback! и мы закончили, удалив этот вызов callback из цикла событий.

Это оставляет цикл событий с еще двумя функциями:

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 из определенной библиотеки, то, что вы написали или каким-либо другим, невозможно точно сказать, что он будет делать в этом случае. Есть только 2 возможности, так что вот они:

// 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, но и параллельно друг другу, поэтому здесь не важно, какой из них был вызван первым, важно, какой из них был первым, и получил dibs в цикле событий.

Какое бы ни было выполнено первое чтение, первое будет выполнено. поэтому, не допуская ошибок, мы распечатаем консоль, а затем напишем ответ на соответствующий запрос, содержащийся в лексической области.

Когда эта отправка будет возвращена, начнется следующий обратный вызов readImage: консольный журнал и запись в ответ.

на этом этапе потоки readImage скончались, а цикл событий пуст, но поток, содержащий привязку к порту сервера, поддерживает процесс, ожидание чего-то еще, чтобы добавить в цикл событий, и цикл для продолжения.

Надеюсь, это поможет вам понять механику асинхронного характера примера, который вы предоставили

Ответ 2

Для каждого входящего запроса узел будет обрабатывать его один за другим. Это означает, что должен быть порядок, как и очередь, сначала в первой подаче. Когда узел начинает обрабатывать запрос, будет выполняться весь синхронный код, а асинхронный будет передаваться в рабочий поток, поэтому узел может начать обрабатывать следующий запрос. Когда асинхронная часть будет выполнена, она вернется в основной поток и продолжит движение.

Поэтому, когда ваш синхронный код занимает слишком много времени, вы блокируете основной поток, узел не сможет обрабатывать другой запрос, его легко проверить.

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

Узел не начнет обработку следующего запроса до завершения всей синхронной части. Поэтому люди не блокируют основной поток.

Ответ 3

Вы можете просто создать дочерний процесс, переместив функцию readImage() в другой файл, используя fork().

Родительский файл, 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);

Выше статьи могут быть полезны для вас.

В родительском файле выше, мы fork child.js (который будет выполнять файл с помощью команды node), а затем мы слушаем событие message. Событие message будет излучаться всякий раз, когда ребенок использует process.send, которые выполняются каждую секунду.

Чтобы передать сообщения от родителя к дочернему, мы можем выполнить функцию send на самом forked-объекте, а затем в дочернем скрипте мы можем прослушать событие message на глобальном объекте process.

При выполнении файла parent.js выше, сначала он отправляет объект { hello: 'world' } должен быть распечатан с помощью разветвленного дочернего процесса, а затем разветвленный дочерний процесс будет посылать значение счетчика с добавочным счетчиком каждую секунду, которое должно быть напечатано родительским процесс.

Ответ 4

Существует ряд статей, объясняющих это, например, этот

nodejs и коротким является то, что nodejs самом деле не однопоточное приложение, его иллюзия. Диаграмма в верхней части приведенной выше ссылки объясняет это достаточно хорошо, однако в качестве сводки

  • NodeJS event-loop работает в одном потоке
  • Когда он получает запрос, он передает запрос на новый поток

Таким образом, в вашем коде у вашего запущенного приложения будет, например, PID 1. Когда вы получаете запрос T1, он создает PID 2, который обрабатывает этот запрос (занимает 1 минуту). Пока это работает, вы получаете запрос T2, который порождает PID 3, также занимает 1 минуту. Оба PID 2 и 3 завершатся после завершения их задачи, однако PID 1 продолжит прослушивание и передачу событий по мере их поступления.

Таким образом, NodeJS являющийся "однопоточным", имеет значение true, но его просто прослушиватель событий. Когда события слышны (запросы), они передают их в пул потоков, которые выполняются асинхронно, что означает, что он не блокирует другие запросы.

Ответ 5

Интервал V8 JS (т.е. Узел) в основном однопоточный. Но процессы, которые он запускает, могут быть асинхронными, например: 'fs.readFile'.

По мере запуска экспресс-сервера он будет открывать новые процессы, поскольку он должен выполнять запросы. Таким образом, функция readImage будет запущена (обычно асинхронно), что означает, что они вернутся в любом порядке. Однако сервер будет управлять тем, какой ответ отправляется на этот запрос автоматически.

Таким образом, вам не нужно будет управлять тем, какой ответ readImage отправляется на какой запрос.

Таким образом, в основном, T1 и T2 не возвращаются одновременно, это практически невозможно. Они оба сильно зависят от файловой системы, чтобы завершить "чтение", и они могут закончить в ЛЮБОЙ ЗАКАЗ (это невозможно предсказать). Обратите внимание, что процессы обрабатываются уровнем ОС и по своей природе многопоточны (на современном компьютере).

Если вы ищете систему очередей, это не должно быть слишком сложно реализовать/обеспечить, чтобы изображения читались/возвращались в том порядке, в котором они запрашиваются.

Ответ 6

Поскольку на Marcus больше нечего добавить к предыдущему ответу - вот графическое изображение, объясняющее однопоточный механизм цикла событий:

enter image description here