Почему setImmediate() выполняется перед fs.readFile() в работах Nodejs Event Loop?
Я прочитал много связанных документов. Но я до сих пор не могу понять, как это работает.
const fs = require('fs')
const now = Date.now();
setTimeout(() => console.log('timer'), 10);
fs.readFile(__filename, () => console.log('readfile'));
setImmediate(() => console.log('immediate'));
while(Date.now() - now < 1000) {
}
const now = Date.now();
setImmediate(() => console.log('immediate'));
setTimeout(() => console.log('timer'), 10);
while(Date.now() - now < 1000) {
}
Я думаю, что первый кусок кода должен войти:
readfile
immediate
И второй кусок кода логов.
timer
immediate
Я думаю, что это нормально.
Проблема: я не понимаю, почему первый кусок кода журналов
immediate
readfile
Я думаю, что файл был прочитан полностью, и его функция обратного вызова ставит в очередь очередь обратных вызовов фазы ввода / вывода через 1 секунду.
И тогда я думаю, что цикл событий переместится в timers(none)
,I/O callbacks(fs.readFile's callback)
,idle/prepare(none)
,poll(none)
,check(setImmediate's callback)
и наконец close callbacks(none)
в порядке, но результат таков setImmediate()
все еще беги первым.
2 ответа
Вы видите поведение, потому что в цикле событий есть несколько типов очередей, и система запускает события в порядке, соответствующем их типу. Это не просто гигантская очередь событий, в которой все выполняется в порядке FIFO в зависимости от того, когда он был добавлен в очередь событий. Вместо этого ему нравится запускать все события одного типа (до предела), переходить к следующему типу, запускать все эти и так далее.
Кроме того, события ввода / вывода добавляются в их очередь только в одной конкретной точке цикла, поэтому они приводятся в определенный порядок. Это причина того, что setImmediate()
обратный вызов выполняется до readFile()
обратный вызов, даже если оба готовы пойти, когда while
цикл сделан.
И тогда я думаю, что цикл обработки событий переместится на таймеры (нет), обратные вызовы ввода / вывода (обратный вызов fs.readFile), idle / prepare (нет), poll (нет), check (обратный вызов setImmediate) и, наконец, закроет обратные вызовы (нет) в порядок, но результат состоит в том, что setImmediate () все еще выполняется первым.
Проблема заключается в том, что на этапе обратных вызовов I/O цикла событий запускаются обратные вызовы I/O, которые уже находятся в очереди событий, но не попадают в очередь событий автоматически, когда они завершены. Вместо этого они помещаются в очередь событий только позже в процессе в I/O poll
шаг (см. схему ниже). Итак, в первый раз на этапе обратных вызовов I/O еще нет никаких обратных вызовов I/O, поэтому вы не получите readfile
вывод, когда вы думаете, что будете.
Но setImmediate()
обратный вызов готов в первый раз через цикл обработки событий, поэтому он запускается до того, как readFile()
Перезвоните.
Это отложенное добавление обратных вызовов ввода / вывода, вероятно, объясняет, почему вы удивляетесь, что readFile()
обратный вызов происходит последним, а не перед setImmediate()
Перезвоните.
Вот что происходит, когда while
цикл заканчивается:
- Когда цикл while завершается, он запускается с обратными вызовами таймера и видит, что таймер готов к запуску, поэтому он запускает его.
- Затем он запускает любые обратные вызовы ввода / вывода, которые уже существуют, но их пока нет. Обратный вызов ввода / вывода от
readFile()
еще не было собрано. Это будет собрано позже в этом цикле. - Затем он проходит несколько других этапов и попадает в опрос ввода-вывода. Есть сбор
readFile()
событие обратного вызова и помещает его в очередь ввода / вывода (но пока не запускает). - Затем он переходит на этап checkHandlers, где он запускает
setImmediate()
Перезвоните. - Затем он снова запускает цикл обработки событий. Нет таймеров, поэтому он переходит к обратным вызовам ввода / вывода и, наконец, находит и запускает
readFile()
Перезвоните.
Итак, давайте документируем, что на самом деле происходит в вашем коде, чуть более подробно для тех, кто не так хорошо знаком с процессом цикла событий. Когда вы запускаете этот код (с добавлением времени к выходу):
const fs = require('fs')
let begin = 0;
function log(msg) {
if (!begin) {
begin = Date.now();
}
let t = ((Date.now() - begin) / 1000).toFixed(3);
console.log("" + t + ": " + msg);
}
log('start program');
setTimeout(() => log('timer'), 10);
setImmediate(() => log('immediate'));
fs.readFile(__filename, () => log('readfile'));
const now = Date.now();
log('start loop');
while(Date.now() - now < 1000) {}
log('done loop');
Вы получаете этот вывод:
0.000: start program
0.004: start loop
1.004: done loop
1.005: timer
1.006: immediate
1.008: readfile
Я добавил время в секундах относительно времени запуска программы, чтобы вы могли видеть, когда все выполняется.
Вот что происходит:
- Таймер запущен и установлен на 10 мсек, другой код продолжает работать
fs.readFile()
операция запущена, другой код продолжает выполнятьсяsetImmediate()
зарегистрирован в системе событий и его событие находится в соответствующей очереди событий, другой код продолжает выполнятьсяwhile
цикл начинает цикл- В течение
while
петля,fs.readFile()
заканчивает свою работу (работает в фоновом режиме). Это событие готово, но еще не в соответствующей очереди событий (подробнее об этом позже) while
цикл заканчивается через 1 секунду цикла, и эта начальная последовательность Javascript завершается и возвращается в систему- Теперь интерпретатору необходимо получить "следующее" событие из цикла событий. Но все типы событий не рассматриваются одинаково. Система событий имеет особый порядок, что она обрабатывает различные типы событий в очереди. В нашем случае здесь сначала обрабатывается событие таймера (я объясню это в следующем тексте). Система проверяет, не истек ли срок действия таймеров, и готовы ли они вызвать обратный вызов. В этом случае он находит, что наш таймер "истек" и готов к работе.
- Таймер обратного вызова вызывается, и мы видим консольное сообщение
timer
, - Таймеров больше нет, поэтому цикл обработки событий переходит к следующему этапу. Следующий этап цикла обработки событий - запуск любых ожидающих обратных вызовов ввода-вывода. Но в очереди событий пока нет ожидающих обратных вызовов ввода / вывода. Хотя
readFile()
уже сделано, его еще нет в очереди - Затем следующий шаг - собрать все завершенные события ввода-вывода и подготовить их к запуску. Здесь
readFile()
событие будет собрано (хотя еще не запущено) и помещено в очередь событий ввода-вывода. - Тогда следующим шагом будет запуск любого
setImmediate()
обработчики, ожидающие рассмотрения. Когда он это делает, мы получаем выводimmediate
, - Затем следующим шагом в процессе события является запуск любых близких обработчиков (здесь нет никого, чтобы запустить).
- Затем цикл событий начинается снова, проверяя таймеры. Нет ожидающих таймеров для запуска.
- Затем цикл обработки запускает любые ожидающие обратные вызовы ввода / вывода. Здесь
readFile()
обратный вызов работает, и мы видимreadfile
в консоли. - У программы больше нет событий для ожидания, поэтому она выполняется.
Сам цикл обработки событий представляет собой серию очередей для разных типов событий и (за некоторыми исключениями) каждая очередь обрабатывается перед переходом к очереди следующего типа. Это вызывает группирование событий (таймеры в одной группе, ожидающие обратные вызовы ввода / вывода в другой группе, setImmediate()
в другой группе и тд). Это не строгая очередь FIFO среди всех типов. События FIFO в группе. Но все ожидающие обратные вызовы таймера (до некоторого предела, чтобы один тип события не мог бесконечно замкнуть цикл событий) обрабатываются перед другими типами обратных вызовов.
Вы можете увидеть базовую структуру на этой диаграмме:
что происходит из этой очень отличной статьи. Если вы действительно хотите понять все эти вещи, прочитайте эту ссылочную статью несколько раз.
Что изначально меня удивило, так это почему readFile
всегда приходит в конце. Это потому, что хотя readFile()
операция выполнена, она не сразу ставится в очередь. Вместо этого в цикле событий есть этап, на котором собираются завершенные события ввода / вывода (которые будут обработаны в следующем цикле через цикл событий) и setImmediate()
события обрабатываются в конце текущего цикла перед только что собранными событиями ввода / вывода. Это делает readFile()
обратный вызов идти после setImmediate()
обратный вызов, даже если они оба готовы к работе во время цикла while.
И, кроме того, не имеет значения, в каком порядке вы выполняете readFile()
и setImmediate()
, Потому что они оба готовы идти до while
Цикл завершен, порядок их выполнения определяется последовательностью, хотя цикл обработки событий выполняет разные типы событий, а не точно, когда они закончили.
Во втором блоке кода вы удаляете readFile()
и положить setImmediate()
перед setTimeout()
, Используя мою синхронизированную версию, это было бы так:
const fs = require('fs')
let begin = 0;
function log(msg) {
if (!begin) {
begin = Date.now();
}
let t = ((Date.now() - begin) / 1000).toFixed(3);
console.log("" + t + ": " + msg);
}
log('start program');
setImmediate(() => log('immediate'));
setTimeout(() => log('timer'), 10);
const now = Date.now();
log('start loop');
while(Date.now() - now < 1000) {}
log('done loop');
И он генерирует этот вывод:
0.000: start program
0.003: start loop
1.003: done loop
1.005: timer
1.008: immediate
Объяснение аналогичное (на этот раз немного сокращено, так как много деталей было объяснено ранее).
setImmediate()
зарегистрирован в соответствующей очереди.setTimeout()
зарегистрирован в очереди таймера.- Цикл while работает в течение 1000 мс
- Код завершает выполнение и возвращает управление обратно в систему
- Система запускается в верхней части логики событий, которая начинается с событий таймера. Таймер, который мы запустили ранее, теперь сделан, поэтому он запускает обратный вызов и регистрирует
timer
, - При отсутствии таймеров цикл обработки событий проходит через несколько других типов очередей событий, пока не доберется до места выполнения.
setImmediate()
обработчики и логиimmediate
,
Если у вас есть расписание нескольких элементов для запуска в обратном вызове ввода / вывода, например:
// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
Затем вы получаете немного другое поведение, потому что setTimeout()
а также setImmediate()
будет запланировано, когда цикл событий находится в другой части своего цикла. В этом конкретном примере setImmediate()
всегда будет выполняться ДО таймера, поэтому вывод будет:
immediate
timeout
На приведенной выше блок-схеме вы можете видеть, где находится шаг "выполнить завершенные обработчики ввода-вывода". Поскольку setTimeout()
а также setImmediate()
вызовы будут запланированы из обработчика ввода-вывода, они будут запланированы на этапе "Выполнить завершенные обработчики ввода-вывода" цикла обработки событий. Следуйте потоку событий цикла, setImmediate()
будет обслуживаться в фазе "проверки обработчиков" до того, как цикл обработки событий вернется к таймерам обслуживания.
Если setImmediate()
а также setTimeout()
запланированы в другой точке цикла событий, тогда таймер может срабатывать до setImmediate()
что происходит в предыдущем примере. Таким образом, относительная синхронизация этих двух параметров зависит от того, в какой фазе находится цикл обработки событий при вызове функций.
setTimeout(() => console.log('timer'), 10);
fs.readFile(__filename, () => console.log('readfile'));
setImmediate(() => console.log('immediate'));
while(Date.now() - now < 1000) {
}
объяснение
setTimeout
графики должны быть помещены в цикл событий после 10 мс.Начинается асинхронное чтение файла.
Нестандартный
setImmediate
планирует показывать вывод консоли, ломая длинные процессы.Запускается цикл блокировки в одну секунду. Ничего в консоли пока нет.
setImmediate
печатьimmediate
консольное сообщение во время цикла.Чтение файла заканчивается, и обратный вызов выполняется даже после
while
цикл окончен. Консольный выводreadfile
там сейчас.Наконец, консольное сообщение
timer
распечатывается примерно через 10 секунд.
Что стоит отметить
Ни одна из вышеперечисленных команд (кроме цикла) не является синхронной. Они что-то планируют и сразу переходят к следующей команде.
Функции обратного вызова вызываются только после завершения текущего выполнения блокировки.
Команды тайм-аута не гарантированно выполняются с указанным интервалом. Гарантия заключается в том, что они будут работать в любое время после интервала.
setImmediate
очень экспериментально.