Другое

Запуск async/await функций параллельно в JavaScript/Node.js

Узнайте, как запускать async/await функции параллельно в JavaScript/Node.js с помощью Promise.all() для ускорения кода и повышения высокую производительность.

Как вызвать async/await функции параллельно в JavaScript/Node.js?

В ES7/ES2016 несколько операторов await выполняются последовательно, а не параллельно, как цепочка .then() с промисами. Например:

javascript
await someCall();
await anotherCall();

Будет ли anotherCall() вызван только после завершения someCall()? Какой самый элегантный способ вызвать асинхронные функции параллельно?

Я ищу решение, работающее в Node.js, желательно с использованием библиотеки async или другого стандартного подхода.

Примечание: меня не интересуют решения с генераторами, так как я задаю вопрос о более общем случае.

Для выполнения функций async/await параллельно в JavaScript/Node.js следует использовать Promise.all(), чтобы запускать несколько асинхронных операций одновременно, а не последовательно. Такой подход значительно повышает производительность, позволяя операциям выполняться параллельно, а не ждать завершения каждой перед началом следующей.

Содержание

Проблема последовательного await

Когда вы используете несколько операторов await последовательно, JavaScript ждёт завершения каждой операции, прежде чем начать следующую. Это создаёт эффект «водопада», где каждый асинхронный вызов зависит от завершения предыдущего.

javascript
// Последовательное выполнение — медленно
async function sequentialCalls() {
    const result1 = await someCall();      // Ждём завершения
    const result2 = await anotherCall();   // Начинается только после result1
    const result3 = await thirdCall();     // Начинается только после result2
    return { result1, result2, result3 };
}

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

Решение с Promise.all()

Самый элегантный и стандартный способ параллельного выполнения — использовать Promise.all(). Согласно Mozilla Developer Network, Promise.all() запускает промисы одновременно, так что пользователи не ждут загрузки цен, прежде чем получить результат.

javascript
// Параллельное выполнение — быстро
async function parallelCalls() {
    const [result1, result2, result3] = await Promise.all([
        someCall(),
        anotherCall(),
        thirdCall()
    ]);
    return { result1, result2, result3 };
}

Ключевые преимущества Promise.all():

  • Параллельное выполнение: все промисы запускаются сразу
  • Сохранение порядка: результаты возвращаются в том же порядке, что и входной массив
  • Обработка ошибок: быстро завершается при первом отклонении любого промиса
  • Чистый синтаксис: использует деструктуризацию массива для удобного доступа к результатам

Как объясняет DEV Community, чтобы раскрыть потенциал async/await в параллельном выполнении, можно воспользоваться API Promise и удобным методом Promise.all(), который позволяет запускать несколько задач одновременно.

Параллельная функция библиотеки async

Для более сложных сценариев библиотека async предоставляет функцию parallel, которая обеспечивает дополнительный контроль над выполнением. Согласно документации async, функция parallel запускает массив функций параллельно, не дожидаясь завершения предыдущей.

javascript
const async = require('async');

async.parallel([
    function(callback) {
        // someCall
        someCall().then(result => callback(null, result));
    },
    function(callback) {
        // anotherCall  
        anotherCall().then(result => callback(null, result));
    },
    function(callback) {
        // thirdCall
        thirdCall().then(result => callback(null, result));
    }
], function(err, results) {
    // results теперь массив [result1, result2, result3]
});

Функция async.parallel также поддерживает объектную нотацию:

javascript
async.parallel({
    task1: function(callback) {
        someCall().then(result => callback(null, result));
    },
    task2: function(callback) {
        anotherCall().then(result => callback(null, result));
    }
}, function(err, results) {
    // results.task1 и results.task2
});

Как отмечает Better Stack Community, чтобы вызывать функции async/await параллельно в JavaScript, можно использовать Promise.all для одновременного выполнения нескольких асинхронных функций и ожидания их завершения.


Соображения производительности

При выполнении асинхронных функций параллельно учитывайте следующие факторы производительности:

Сетевые запросы: для API‑запросов параллельное выполнение может значительно сократить общее время ответа. Согласно Stack Overflow, если каждый запрос является сетевым, await someResult будет ждать завершения, прежде чем запустить await anotherResult. В Promise.all оба await запускаются сразу, и они выполняются параллельно.

Использование ресурсов: будьте внимательны, что параллельное выполнение потребляет больше ресурсов одновременно. Для CPU‑зависимых операций в Node.js помните, что Node.js однопоточный, поэтому операции, казалось бы, параллельные, на самом деле чередуются в цикле событий.

Ограничения параллелизма: при работе с большим количеством операций рассмотрите ограничение параллелизма:

javascript
async function limitedParallel(tasks, limit = 5) {
    const results = [];
    for (let i = 0; i < tasks.length; i += limit) {
        const batch = tasks.slice(i, i + limit);
        const batchResults = await Promise.all(batch);
        results.push(...batchResults);
    }
    return results;
}

Подходы к обработке ошибок

Разные методы параллельного выполнения обрабатывают ошибки по‑разному:

Promise.all(): быстро завершается – если любой промис отклонён, сразу отклоняется с этой ошибкой:

javascript
Promise.all([
    Promise.resolve(1),
    Promise.reject(new Error('Failed')),
    Promise.resolve(3)
]).catch(err => {
    // Ошибка поймана, значения [1, 3] недоступны
});

Promise.allSettled(): ждёт завершения всех промисов независимо от ошибок:

javascript
Promise.allSettled([
    someCall(),
    anotherCall(),
    thirdCall()
]).then(results => {
    // results содержит статус и value/reason для каждого промиса
    const successful = results.filter(r => r.status === 'fulfilled');
    const failed = results.filter(r => r.status === 'rejected');
});

async.parallel(): аналогично Promise.all() – быстро завершается по умолчанию, но можно настроить { concurrency: n } для контролируемого параллелизма.


Практические примеры

Пример 1: Параллельные API‑запросы

javascript
async function fetchUserData(userId) {
    const [profile, posts, comments] = await Promise.all([
        fetch(`/api/users/${userId}`),
        fetch(`/api/users/${userId}/posts`),
        fetch(`/api/users/${userId}/comments`)
    ]);
    
    return {
        profile: await profile.json(),
        posts: await posts.json(),
        comments: await comments.json()
    };
}

Пример 2: Параллельные операции с файлами

javascript
const fs = require('fs').promises;

async function processFiles(filePaths) {
    const fileContents = await Promise.all(
        filePaths.map(filePath => fs.readFile(filePath, 'utf8'))
    );
    
    return fileContents.map(content => {
        // Обрабатываем каждый файл
        return content.split('\n').length;
    });
}

Пример 3: Смешанные операции с async.parallel

javascript
const async = require('async');

async function processData() {
    try {
        const [dbResult, apiResult, cacheResult] = await new Promise((resolve, reject) => {
            async.parallel({
                db: callback => {
                    database.query('SELECT * FROM users', (err, result) => {
                        callback(err, result);
                    });
                },
                api: callback => {
                    axios.get('https://api.example.com/data')
                        .then(response => callback(null, response.data))
                        .catch(callback);
                },
                cache: callback => {
                    redis.get('cached_data', (err, result) => {
                        callback(err, result);
                    });
                }
            }, (err, results) => {
                if (err) reject(err);
                else resolve([results.db, results.api, results.cache]);
            });
        });
        
        return { dbResult, apiResult, cacheResult };
    } catch (error) {
        console.error('Ошибка при параллельном выполнении:', error);
        throw error;
    }
}

Как демонстрирует Futurestud.io, этот подход сокращает объём кода, поскольку Promise.all() инкапсулирует всё, что нужно. Запуск списка промисов параллельно может вызвать ошибки, но преимущества в производительности значительны.

Источники

  1. JavaScript - Call async/await functions in parallel - Stack Overflow
  2. Promise.all() - JavaScript | MDN
  3. Mastering Parallel Execution with async/await in JavaScript (Node.js) for Beginners - DEV Community
  4. Call async/await Functions in Parallel | Better Stack Community
  5. Node.js — Run Async Functions/Promises in Parallel - Futurestud.io
  6. async - Documentation
  7. Does JavaScript Promise.all() run in parallel or sequential? | Leo’s dev blog
  8. Understanding Node.js Async Flows: Parallel, Serial, Waterfall and Queues - Velotio

Заключение

Чтобы эффективно вызывать функции async/await параллельно в JavaScript/Node.js:

  • Используйте Promise.all() как самый элегантный и стандартный способ параллельного выполнения
  • Рассмотрите async.parallel(), когда нужен более тонкий контроль над обработкой ошибок или работа с callback‑стилем
  • Обрабатывайте ошибки корректно, используя Promise.allSettled() при необходимости получить все результаты независимо от сбоев
  • Будьте внимательны к использованию ресурсов при выполнении большого количества операций параллельно
  • Ограничьте параллелизм при работе с большим числом задач, чтобы не перегрузить систему

Ключевой вывод: промисы в JavaScript «жадные» – они начинают выполняться сразу при создании, а не при ожидании. Это означает, что вы можете создать все промисы сначала, а затем дождаться их выполнения вместе с помощью Promise.all(), чтобы достичь истинного параллельного выполнения.

Авторы
Проверено модерацией
Модерация