Параллельный вызов функций async/await в Node.js: решения
Быстрый ответ
Для реализации параллельного выполнения функций async/await используется метод Promise.all
. Этот метод работает с массивом промисов и возвращает результат выполнения всех промисов.
Иллюстрируем на примере:
async function fetchData() { /* ... */ }
async function fetchMoreData() { /* ... */ }
const [data, moreData] = await Promise.all([fetchData(), fetchMoreData()]);
После выполнения промисов значения data
и moreData
будут содержать результаты вызова соответствующих функций.
Обработка ошибок и выбор альтернатив
С блоками try...catch
для обработки ошибок мы имеем больше гибкости, нежели с Promise.all
. Последний немедленно прерывает выполнение при возникновении ошибки в любом из промисов. Детализированный отчёт об ошибках доступен при использовании метода Promise.allSettled
. Однако стоит учесть, что этот метод является относительно новым и не поддерживается в Internet Explorer. В качестве альтернативы можно применить следующий подход с использованием try...catch
:
async function processInParallel() {
try {
const [data, moreData] = await Promise.all([fetchData(), fetchMoreData()]);
// Далее следует обработка данных
} catch (error) {
// Здесь обрабатываются ошибки
}
}
Если необходимо продолжить выполнение кода независимо от результатов выполнения промисов, рекомендуется обрабатывать ошибки для каждого промиса отдельно или использовать метод Promise.allSettled
:
async function fetchAllData() {
const results = await Promise.allSettled([fetchData(), fetchMoreData()]);
for (const result of results) {
if (result.status === 'fulfilled') {
// Всё выполнено успешно!
} else {
// Ошибка. Необходима обработка
}
}
}
Визуализация
Параллельное выполнение функций async/await
можно сравнить с эстафетой: все функции стартуют одновременно и продвигаются вперёд, не ожидая друг друга:
Эстафета асинхронности 🏃♂️🏃♀️🏃
Старт: 🏁 [Функция A, Функция B, Функция C]
Стартовый выстрел! 🎯 (Все начинают одновременно)
Итог: 🎉 Функции выполняются параллельно, стремясь выполниться как можно быстрее!
Основная идея: 🎯 Функции не ожидают завершения друг друга и движутся как можно быстрее, что уменьшает общее время выполнения. 🏃♂️💨
Метрики производительности и стратегия отката
При параллельном выполнении задач, как правило, производительность улучшается. Мы можем это измерить с помощью console.time
и console.timeEnd
:
console.time('параллельная загрузка данных');
const [data, moreData] = await Promise.all([fetchData(), fetchMoreData()]);
console.timeEnd('параллельная загрузка данных'); // Выводит время выполнения
При использовании Promise.all
отсутствует возможность отменить выполнение после его начала. Если одна задача зависит от другой и в случае неудачи требуется откат, вам придётся учитывать это заранее и разрабатывать стратегию поведения в случае возникновения ошибок.
Увеличиваем скорость выполнения, применяя параллельность
Не только API-вызовы могут выполняться параллельно: в некоторых случаях это также применимо для чтения или записи файлов, выполнения запросов к базе данных. В Node.js для этого можно использовать набор утилит от библиотеки async
, таких как eachLimit
.
Если между задачами есть сложная логика или зависимости, требуется создание отдельного промиса для каждой асинхронной функции:
const timerPromise = (ms, value) => new Promise(resolve => setTimeout(() => resolve(value), ms));
const runConcurrently = async () => {
const promises = [
timerPromise(500, 'Первый'),
timerPromise(1000, 'Второй'),
timerPromise(1500, 'Третий')
];
return await Promise.all(promises);
}
runConcurrently().then(console.log); // ["Первый", "Второй", "Третий"] примерно через 1500 мс
Последовательное и параллельное выполнение: что выбрать?
Важно разбираться в отличиях между параллельным и последовательным выполнением. Использование async/await
в цикле приводит к последовательному выполнению, в то время как Promise.all
обеспечивает параллельное выполнение. Рассмотрим примеры:
Последовательное выполнение (Выполнение тогда, когда подошла очередь):
for (const asyncFunc of [fetchData, fetchMoreData]) {
await asyncFunc(); // "Подождите, пожалуйста... Ваша очередь."
}
Параллельное выполнение (Выполнение всех задач одновременно):
await Promise.all([fetchData(), fetchMoreData()]); // "Все сразу!"
Дружественная обработка ошибок
Готовьтесь к тому, что при использовании Promise.all
сбой хотя бы одного промиса приведёт к отказу всех других. Promise.allSettled
используется, когда требуется дожидаться завершения всех промисов, прежде чем продолжать выполнение кода.
Полезные материалы
- Использование промисов – JavaScript | MDN — Подробное руководство по работе с промисами в JavaScript.
- async function – JavaScript | MDN — Полное описание функций async.
- Promise API — Примеры и способы использования Promise API.
- Promise.all() – JavaScript | MDN — Синхронизация асинхронных операций с помощью Promise.all().
- Async hooks | Документация Node.js v21.6.1 — Ресурс для отладки и мониторинга асинхронных операций в Node.js.
- javascript – Использование async/await с циклом forEach – Stack Overflow — Раcсмотрение проблем и способов их решения при использовании async/await в циклах forEach.
- Util | Документация Node.js v21.6.1 — Преобразование функций с колбэками в промисы для обеспечения совместимости с async/await.