Обіцянки з обмеженим паралелізмом у JavaScript

pic

Фото Ірвана Сміта на Unsplash

Коли працюєш з великою кількістю асинхронних операцій у JavaScript, таких як API запити або обробка файлів, однією з поширених проблем є надмірна кількість обіцянок (promises), які виконуються одночасно. Спроба виконати всі задачі одночасно, використовуючи Promise.allSettled або Promise.all, може перевантажити вашу систему або вдарити по лімітах API і/або ресурсах. Одним зі способів обмежити кількість обіцянок, що виконуються одночасно, є обмеження паралелізму.

У цьому пості ми розглянемо, як реалізувати власне рішення, а також обговоримо альтернативи, такі як Promise.map від Bluebird. Однак ми також зважимо на те, чому встановлення бібліотеки, такої як Bluebird, може бути не ідеальним для маленьких проектів.

Розуміння проблеми

Припустимо, вам потрібно завантажити 1000 зображень. Якщо виконати всі 1000 завдань паралельно, це може призвести до:

  1. Перевищення лімітів запитів і помилок.
  2. Споживання надмірних ресурсів системи, що може призвести до погіршення продуктивності.

Рішення: Потрібно обробляти обіцянки пакетами, обмежуючи кількість тих, що виконуються одночасно.

Рішення 1: Promise.map від Bluebird

Bluebird — одна з найпопулярніших бібліотек для роботи з обіцянками, яка пропонує багато розширених можливостей, зокрема вбудований метод Promise.map, який дозволяє контролювати паралелізм.

Ось приклад того, як це працює:

// Імпортуємо пакет Bluebird  
const Bluebird = require('bluebird');  

// Створюємо список фейкових обіцянок  
const promiseList = Array.from({ length: 1000 }, (_, i) => () => {  
 return new Promise((resolve) => {  
 setTimeout(() => {  
 console.log(`Завдання ${i + 1} виконано`);  
 resolve(`Результат завдання ${i + 1}`);  
 }, Math.random() * 2000);  
 });  
});  

// Виконуємо з обмеженням на паралелізм 3  
Bluebird.map(promiseList, (task) => task(), { concurrency: 3 })  
 .then((results) => console.log("Всі завдання виконано:", results));

Виведення:

Завдання 1 виконано  
Завдання 2 виконано  
Завдання 3 виконано  
Завдання 4 виконано  
Завдання 5 виконано  
...  
Всі завдання виконано: ["Результат завдання 1", ..., "Результат завдання 1000"]

Переваги використання Bluebird

  • Зручність: Promise.map компактний і керує паралелізмом з мінімумом шаблонного коду.
  • Надійність: Bluebird — це зріла бібліотека, яка ефективно управляє обіцянками.

Чому не використовувати Bluebird?
Bluebird пропонує відмінне рішення, але це має свої недоліки:

  1. Додаткова залежність: Встановлення Bluebird додає до вашого проекту бібліотеку розміром 21кБ. Якщо ви використовуєте лише Promise.map, це може здатися надмірним.
  2. Сучасний JavaScript: Вбудовані обіцянки в JavaScript значно покращилися. Для маленьких або простих проектів використання бібліотек для таких функцій, як паралелізм, не завжди є необхідним.
  3. Продуктивність: Хоча Bluebird оптимізовано для швидкості, додавання бібліотеки може бути не виправданим, якщо ви працюєте з меншою кількістю обіцянок.

Рішення 2: Власне рішення з пулом обіцянок

Якщо ви не хочете додавати цілу бібліотеку тільки для управління паралелізмом, ви можете створити власний пул обіцянок в JavaScript.
Ось проста реалізація:

Код:

function promisePool(tasks, { limit = 5}) {  
 return new Promise((resolve) => {  
 let activeCount = 0;  
 let currentIndex = 0;  
 const results = new Array(tasks.length);  

 function runNext() {  
 // Усі задачі виконано  
 if (currentIndex >= tasks.length && activeCount === 0) {  
 resolve(results);  
 return;  
 }  

 // Досягнуто ліміту або немає завдань для запуску  
 if (activeCount >= limit || currentIndex >= tasks.length) {  
 return;  
 }  

 const taskIndex = currentIndex++;  
 const task = tasks[taskIndex];  

 activeCount++;  
 Promise.resolve(task())  
 .then((result) => {  
 results[taskIndex] = { status: "fulfilled", value: result };  
 })  
 .catch((error) => {  
 results[taskIndex] = { status: "rejected", reason: error };  
 })  
 .finally(() => {  
 activeCount--;  
 runNext(); // Запустити наступну задачу  
 });  
 }  

 // Запускаємо з початковим пулом обіцянок  
 for (let i = 0; i < limit; i++) {  
 runNext();  
 }  
 });  
}

Приклад використання:

// Массив функцій, що повертають обіцянки для виконання.  
const tasks = Array.from({ length: 1000 }, (_, i) => () => {  
 return new Promise((resolve) => {  
 setTimeout(() => {  
 console.log(`Завдання ${i + 1} виконано`);  
 resolve(`Результат завдання ${i + 1}`);  
 }, Math.random() * 2000);  
 });  
});  

// Обмежити паралелізм до 3 завдань  
const options = { limit: 3 };  
promisePool(tasks, options)  
 .then((results) => console.log("Всі завдання виконано:", results));

Виведення:

Завдання 1 виконано  
Завдання 2 виконано  
Завдання 3 виконано  
Завдання 4 виконано  
...  
Всі завдання виконано: [  
 { status: "fulfilled", value: "Результат завдання 1" },  
 ...  
]

Коли використовувати Bluebird або власне рішення?

Сценарій 1: Ви вже використовуєте Bluebird в проекті

Використовуйте Promise.map. Це ефективно і зручно.

Сценарій 2: Вам потрібні розширені утиліти для роботи з обіцянками

Bluebird надає можливості, як-от Promise.reduce та Promise.each.

Сценарій 3: Легкий проект, одна функція

Уникайте Bluebird. Напишіть власне рішення, як, наприклад, реалізація пулу обіцянок.

Підсумки

Використання бібліотек, таких як Bluebird, може значно спростити написання коду, але додавання ще однієї залежності розміром 21kB (77.5kB без стиснення) для однієї функції не є ідеальним, особливо для маленьких проектів. Власне рішення, таке як функція promisePool, може досягти подібних результатів без додавання непотрібних навантажень. Для більших проектів, які вже використовують Bluebird, Promise.map є зручним і надійним вибором.

Завжди враховуйте складність вашого проекту та потребу в додаткових залежностях при прийнятті рішення.

Перекладено з: Promises with Limited Parallelism in JavaScript

Leave a Reply

Your email address will not be published. Required fields are marked *