Этот обзор рассматривает разницу между блокирующими и неблокирующими вызовами в Node.js. Здесь будут упомянуты event loop и libuv, но предварительное знание этих тем не требуется. Предполагается, что читатель обладает базовым пониманием языка JavaScript и паттерна колбэков в Node.js.
Под «I/O» здесь в первую очередь понимается взаимодействие с диском и сетью системы, обеспечиваемое libuv.
Блокировка — это когда выполнение дополнительного JavaScript в процессе Node.js вынуждено ждать завершения не-JavaScript-операции. Так происходит, потому что event loop не способен продолжать выполнять JavaScript, пока идёт блокирующая операция.
В Node.js JavaScript, демонстрирующий низкую производительность из-за интенсивной нагрузки на CPU, а не из-за ожидания не-JavaScript-операции, такой как I/O, обычно не называют блокирующим. Синхронные методы стандартной библиотеки Node.js, использующие libuv, — самые часто применяемые блокирующие операции. У нативных модулей тоже могут быть блокирующие методы.
Все методы I/O в стандартной библиотеке Node.js предоставляют асинхронные
версии, которые являются неблокирующими и принимают функции-колбэки. У некоторых
методов есть и блокирующие аналоги, имена которых оканчиваются на
Sync.
Блокирующие методы выполняются синхронно, а неблокирующие методы выполняются асинхронно.
На примере модуля файловой системы — вот синхронное чтение файла:
const fs = require('node:fs');
const data = fs.readFileSync('/file.md'); // блокируется здесь, пока файл не прочитанА вот эквивалентный асинхронный пример:
const fs = require('node:fs');
fs.readFile('/file.md', (err, data) => {
if (err) {
throw err;
}
});Первый пример выглядит проще второго, но у него есть недостаток: вторая строка блокирует выполнение любого дополнительного JavaScript, пока файл не прочитан целиком. Обратите внимание: в синхронной версии, если выбрасывается ошибка, её нужно поймать, иначе процесс упадёт. В асинхронной версии автор сам решает, должна ли ошибка выбрасываться, как показано.
Немного расширим наш пример:
const fs = require('node:fs');
const data = fs.readFileSync('/file.md'); // блокируется здесь, пока файл не прочитан
console.log(data);
moreWork(); // выполнится после console.logА вот похожий, но не эквивалентный асинхронный пример:
const fs = require('node:fs');
fs.readFile('/file.md', (err, data) => {
if (err) {
throw err;
}
console.log(data);
});
moreWork(); // выполнится перед console.logВ первом примере выше console.log будет вызван до moreWork(). Во
втором примере fs.readFile() неблокирующий, поэтому выполнение JavaScript
может продолжиться, и moreWork() будет вызван первым. Возможность выполнить
moreWork(), не дожидаясь завершения чтения файла, — ключевое проектное
решение, которое позволяет достигать более высокой пропускной способности.
Выполнение JavaScript в Node.js однопоточно, поэтому конкурентность здесь означает способность event loop выполнять функции-колбэки JavaScript после завершения другой работы. Любой код, который должен выполняться конкурентно, должен позволять event loop продолжать работать, пока идут не-JavaScript-операции, такие как I/O.
В качестве примера рассмотрим случай, когда каждый запрос к веб-серверу выполняется 50 мс, и 45 мс из этих 50 мс — это I/O к базе данных, которое можно выполнить асинхронно. Выбор неблокирующих асинхронных операций высвобождает эти 45 мс на запрос для обработки других запросов. Это существенная разница в пропускной способности — всего лишь благодаря выбору неблокирующих методов вместо блокирующих.
Event loop отличается от моделей многих других языков, где для обработки конкурентной работы могут создаваться дополнительные потоки.
Есть паттерны, которых при работе с I/O следует избегать. Рассмотрим пример:
const fs = require('node:fs');
fs.readFile('/file.md', (err, data) => {
if (err) {
throw err;
}
console.log(data);
});
fs.unlinkSync('/file.md');В примере выше fs.unlinkSync(), скорее всего, выполнится раньше
fs.readFile(), что удалит file.md до того, как он будет фактически прочитан.
Лучший способ это написать — полностью неблокирующий и гарантированно
выполняющийся в правильном порядке — такой:
const fs = require('node:fs');
fs.readFile('/file.md', (readFileErr, data) => {
if (readFileErr) {
throw readFileErr;
}
console.log(data);
fs.unlink('/file.md', unlinkErr => {
if (unlinkErr) {
throw unlinkErr;
}
});
});Здесь неблокирующий вызов fs.unlink() помещён внутрь колбэка
fs.readFile(), что гарантирует правильный порядок операций.