Параллельный вызов функций async/await в Node.js: решения

Пройдите тест, узнайте какой профессии подходите

Я предпочитаю
0%
Работать самостоятельно и не зависеть от других
Работать в команде и рассчитывать на помощь коллег
Организовывать и контролировать процесс работы

Быстрый ответ

Для реализации параллельного выполнения функций async/await используется метод Promise.all. Этот метод работает с массивом промисов и возвращает результат выполнения всех промисов.
Иллюстрируем на примере:

JS
Скопировать код
async function fetchData() { /* ... */ }
async function fetchMoreData() { /* ... */ }

const [data, moreData] = await Promise.all([fetchData(), fetchMoreData()]);

После выполнения промисов значения data и moreData будут содержать результаты вызова соответствующих функций.

Кинга Идем в IT: пошаговый план для смены профессии

Обработка ошибок и выбор альтернатив

С блоками try...catch для обработки ошибок мы имеем больше гибкости, нежели с Promise.all. Последний немедленно прерывает выполнение при возникновении ошибки в любом из промисов. Детализированный отчёт об ошибках доступен при использовании метода Promise.allSettled. Однако стоит учесть, что этот метод является относительно новым и не поддерживается в Internet Explorer. В качестве альтернативы можно применить следующий подход с использованием try...catch:

JS
Скопировать код
async function processInParallel() {
  try {
    const [data, moreData] = await Promise.all([fetchData(), fetchMoreData()]);
    // Далее следует обработка данных
  } catch (error) {
    // Здесь обрабатываются ошибки
  }
}

Если необходимо продолжить выполнение кода независимо от результатов выполнения промисов, рекомендуется обрабатывать ошибки для каждого промиса отдельно или использовать метод Promise.allSettled:

JS
Скопировать код
async function fetchAllData() {
  const results = await Promise.allSettled([fetchData(), fetchMoreData()]);
  
  for (const result of results) {
    if (result.status === 'fulfilled') {
      // Всё выполнено успешно!
    } else {
      // Ошибка. Необходима обработка
    }
  }
}

Визуализация

Параллельное выполнение функций async/await можно сравнить с эстафетой: все функции стартуют одновременно и продвигаются вперёд, не ожидая друг друга:

Markdown
Скопировать код
Эстафета асинхронности 🏃‍♂️🏃‍♀️🏃

Старт: 🏁 [Функция A, Функция B, Функция C]

Стартовый выстрел! 🎯 (Все начинают одновременно)

Итог: 🎉  Функции выполняются параллельно, стремясь выполниться как можно быстрее!

Основная идея: 🎯 Функции не ожидают завершения друг друга и движутся как можно быстрее, что уменьшает общее время выполнения. 🏃‍♂️💨

Метрики производительности и стратегия отката

При параллельном выполнении задач, как правило, производительность улучшается. Мы можем это измерить с помощью console.time и console.timeEnd:

JS
Скопировать код
console.time('параллельная загрузка данных');
const [data, moreData] = await Promise.all([fetchData(), fetchMoreData()]);
console.timeEnd('параллельная загрузка данных'); // Выводит время выполнения

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

Увеличиваем скорость выполнения, применяя параллельность

Не только API-вызовы могут выполняться параллельно: в некоторых случаях это также применимо для чтения или записи файлов, выполнения запросов к базе данных. В Node.js для этого можно использовать набор утилит от библиотеки async, таких как eachLimit.

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

JS
Скопировать код
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 обеспечивает параллельное выполнение. Рассмотрим примеры:

Последовательное выполнение (Выполнение тогда, когда подошла очередь):

JS
Скопировать код
for (const asyncFunc of [fetchData, fetchMoreData]) {
  await asyncFunc(); // "Подождите, пожалуйста... Ваша очередь."
}

Параллельное выполнение (Выполнение всех задач одновременно):

JS
Скопировать код
await Promise.all([fetchData(), fetchMoreData()]); // "Все сразу!"

Дружественная обработка ошибок

Готовьтесь к тому, что при использовании Promise.all сбой хотя бы одного промиса приведёт к отказу всех других. Promise.allSettled используется, когда требуется дожидаться завершения всех промисов, прежде чем продолжать выполнение кода.

Полезные материалы

  1. Использование промисов – JavaScript | MDN — Подробное руководство по работе с промисами в JavaScript.
  2. async function – JavaScript | MDN — Полное описание функций async.
  3. Promise API — Примеры и способы использования Promise API.
  4. Promise.all() – JavaScript | MDN — Синхронизация асинхронных операций с помощью Promise.all().
  5. Async hooks | Документация Node.js v21.6.1 — Ресурс для отладки и мониторинга асинхронных операций в Node.js.
  6. javascript – Использование async/await с циклом forEach – Stack Overflow — Раcсмотрение проблем и способов их решения при использовании async/await в циклах forEach.
  7. Util | Документация Node.js v21.6.1 — Преобразование функций с колбэками в промисы для обеспечения совместимости с async/await.