Concurrency Control with Promises

Concurrency Control with Promises

ยท

4 min read

Question

You are given an array of asynchronous tasks that need to be executed concurrently using promises. Each task is represented as a function that returns a promise. However, you need to ensure that no more than n tasks are running concurrently at any given time.

Write a function concurrentPromises(tasks, n) that takes in two arguments:

  1. tasks: An array of asynchronous tasks, where each task is a function that returns a promise.

  2. n: The maximum number of tasks that can run concurrently.

The function should execute all tasks and resolve with an array of their results, in the same order as the original tasks array.

Example:

// Three asynchronous tasks that return promises
function task1() {
  return new Promise(resolve => setTimeout(() => resolve("Task 1"), 2000));
}

function task2() {
  return new Promise(resolve => setTimeout(() => resolve("Task 2"), 1000));
}

function task3() {
  return new Promise(resolve => setTimeout(() => resolve("Task 3"), 3000));
}

const tasks = [task1, task2, task3];
const n = 2;

concurrentPromises(tasks, n)
  .then(results => {
    console.log(results);
    // Output: ["Task 1", "Task 2", "Task 3"]
  })
  .catch(error => {
    console.error(error);
  });

Constraints:

  • You must use native Javascript Promises to implement the solution.

  • Do not use any external libraries or utilities to handle concurrency control (e.g., async, await, or third-party libraries).

  • The tasks array can have a variable number of elements, and n can be any positive integer.

Before jumping to the solution section, please try to think and do it yourself. I am giving you time โณ

Solution

To implement concurrency control, you can create a mechanism that maintains a pool of active promises and a queue of pending tasks. You can then resolve the promises one by one as tasks complete and remove them from the pool, while simultaneously adding new tasks to the pool from the queue to maintain the concurrency limit.

function concurrentPromises(tasks, n) {
  const results = [];

  // Helper function to run the next task
  function runNextTask(index) {
    if (index >= tasks.length) {
      return Promise.resolve();
    }

    const task = tasks[index];
    return task().then((result) => {
      results[index] = result;
      if (index + n <= tasks.length) {
        return runNextTask(index + n);
      }
      return;
    });
  }

  // Create an array of promises for the first n tasks
  const initialPromises = [];
  for (let i = 0; i < Math.min(n, tasks.length); i++) {
    initialPromises.push(runNextTask(i));
  }

  // Wait for all promises to settle (succeed or fail)
  return Promise.allSettled(initialPromises).then(() => results);
}

// Example tasks
function task1() {
  return new Promise((resolve) => setTimeout(() => resolve("Task 1"), 2000));
}

function task2() {
  return new Promise((resolve) => setTimeout(() => resolve("Task 2"), 1000));
}

function task3() {
  return new Promise((resolve) => setTimeout(() => resolve("Task 3"), 3000));
}
function task4() {
  return new Promise((resolve) => setTimeout(() => resolve("Task 4"), 2000));
}
function task5() {
  return new Promise((resolve) => setTimeout(() => resolve("Task 5"), 1000));
}
function task6() {
  return new Promise((resolve) => setTimeout(() => resolve("Task 6"), 4000));
}

const tasks = [task1, task2, task3, task4, task5, task6];
const n = 3;

concurrentPromises(tasks, n)
  .then((results) => {
    console.log(results)
    // Output: ["Task 1", "Task 2", "Task 3", "Task 4", "Task 5", "Task 6"]
  })
  .catch((error) => {
    console.error(error);
  });

In this solution, we use Promise.allSettled to handle concurrency control. The runNextTask function is called recursively to execute the tasks sequentially while ensuring that no more than n tasks are running concurrently. Once all promises for the tasks are settled (either fulfilled or rejected), we collect the results and resolve the main promise with the results array. This ensures that we get the correct results in the order of the original tasks array.

The key to this implementation is using recursion and managing the number of active tasks to control concurrency effectively.

Want to learn more through interview questions, checkout my frontend interview series.

If you liked what you read ๐Ÿง‘โ€๐Ÿซ and got to learn new things, do hit like ๐Ÿ‘ and subscribe ๐Ÿ”– to my newsletter to get instantly notified whenever I drop in new content.

And don't forget to follow ๐Ÿš€ me on

Hashnode - Rajat Jain

Twitter - @rajatexplains

Instagram - @javascript_to_the_rescue

LinkedIn - Rajat Jain

Did you find this article valuable?

Support Rajat Explains by becoming a sponsor. Any amount is appreciated!

ย