Глубокое понимание механизма цикла событий nodejs

Node.js внешний интерфейс JavaScript Promise

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

один поток

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

механизм цикла событий

Секрет способности узла достичь высокого параллелизма заключается в механизме цикла обработки событий, который похож на механизм браузера, но также имеет много отличий. Согласно официальному представлению узла, каждый механизм цикла событий узла содержит 6 этапов:

  • Этап таймеров: на этом этапе выполняется обратный вызов таймера с истекшим сроком действия (setTimeout, setInterval).
  • Фаза обратных вызовов ввода-вывода: обратные вызовы, которые выполняют ввод-вывод (например, файл, сеть)
  • бездействие, этап подготовки: используется внутри узла
  • Стадия опроса: получение новых событий ввода-вывода, здесь узел будет заблокирован при соответствующих условиях.
  • фаза проверки: выполнить обратный вызов setImmediate
  • этап обратных вызовов закрытия: выполнение обратных вызовов событий закрытия, таких как отключение TCP

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

console.log('main');

setImmediate(function() {
    console.log('setImmediate');
});

new Promise(function(resolve, reject) {
    resolve();
}).then(function() {
    console.log('promise.then');
});

Результат выполнения кода:

  1. main
  2. promise.then
  3. setImemediate

setImmediate и process.nextTick

По сравнению со средой браузера в среде узла есть еще две асинхронные операции, setImmediate и process.nextTick. Функция обратного вызова setImmediate выполняется на этапе проверки, что эквивалентно последнему этапу цикла обработки событий. А process.nextTick будет рассматриваться как своего рода микрозадача, как было сказано выше, все задачи микрозадачи будут выполняться после завершения каждого этапа, поэтому process.nextTick имеет аналогичный эффект сокращения очереди, который может быть выполнен до следующего этапа, но это то же самое, что и promise.тогда какой из них выполняется первым? Поэкспериментируйте с фрагментом кода:

console.log('main');

process.nextTick(function() {
    console.log('nextTick')
})

new Promise(function(resolve, reject) {
    resolve();
}).then(function() {
    console.log('promise.then');
});

Результат выполнения кода:

  1. main
  2. nextTick
  3. promise.then

Получается, что process.nextTick будет иметь более высокий приоритет, чем promise.then.

Ловушка голодания process.nextTick

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

Это легко понять, глядя на пример

let i = 0;
setImmediate(function() {
    console.log('setImmediate');
});
function callback() {
    console.log('nextTick' + i++);
    if (i < 1000) {
        process.nextTick(callback);
    }
}
callback();

Результат выполнения есть следующийTick0 следующийTick1 следующийTick2 ... следующийTick999 setImmediate

Обратный вызов setImmediate будет ждать завершения задач process.nextTick перед выполнением.

резюме

1. Механизм событийного цикла узла отличается от механизма браузеров, и есть еще два асинхронных метода, setImmediate и process.nextTick. Поскольку process.nextTick приведет к голоданию ввода-вывода, также рекомендуется официальное использование setImmediate. 2. Несмотря на то, что Node.js является однопоточной архитектурой, он также может обеспечить высокий уровень параллелизма. Причина кроется в его основном механизме цикла событий потока и реализации базового пула потоков. 3. Этот механизм определяет, что узел больше подходит для приложений с интенсивным вводом-выводом, но не для приложений с интенсивным использованием ЦП.