Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Как выполнять асинхронные задачи параллельно, но с ограничением

Короткий ответ

Запустите N обработчиков. Каждый берёт следующую задачу по общему счётчику, пока задачи не кончатся:

async function mapWithLimit(items, limit, fn) {
  const results = new Array(items.length);
  let next = 0;
  const worker = async () => {
    while (next < items.length) {
      const index = next++;
      results[index] = await fn(items[index]);
    }
  };
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
  return results;
}
 
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let running = 0;
let maxRunning = 0;
 
const results = await mapWithLimit([1, 2, 3, 4, 5, 6, 7], 3, async (n) => {
  running++;
  maxRunning = Math.max(maxRunning, running);
  await delay(50);
  running--;
  return n * 10;
});
 
console.log(results.join(', '));
console.log('at most at once:', maxRunning);

Результат:

10, 20, 30, 40, 50, 60, 70
at most at once: 3

С fetch

Так можно скачать сотню страниц, не перегружая сервер:

const ids = Array.from({ length: 100 }, (_, i) => i + 1);
const products = await mapWithLimit(ids, 5, (id) => fetch(`/api/products/${id}`).then((r) => r.json()));

Пачками

Способ проще — пачками: запустить N задач, дождаться всех, затем следующие N. Это чуть медленнее, потому что каждая пачка ждёт свою самую медленную задачу:

const delay = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
const items = [1, 2, 3, 4, 5];
const results = [];
 
for (let i = 0; i < items.length; i += 2) {
  const batch = items.slice(i, i + 2);
  results.push(...(await Promise.all(batch.map((n) => delay(20, n * n)))));
}
 
console.log(results.join(', '));

Результат:

1, 4, 9, 16, 25

Частая ошибка

items.map(fn) запускает все задачи сразу. Promise.all() только ждёт их и ничего не ограничивает:

let running = 0;
let maxRunning = 0;
const task = async () => {
  running++;
  maxRunning = Math.max(maxRunning, running);
  await new Promise((resolve) => setTimeout(resolve, 10));
  running--;
};
 
await Promise.all(Array.from({ length: 50 }, task));
 
console.log(maxRunning);

Результат:

50

Функции из этого ответа

  • Promise.all — Ждёт выполнения всех промисов параллельно.
  • async function — Функция, которая всегда возвращает промис и внутри которой можно использовать await.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.