Efficiently Managing Concurrent Async Tasks with a Promise Pool
Controlling Concurrent Async Operations with a Limited Execution Pool
In modern JavaScript applications, managing multiple asynchronous tasks efficiently is crucial for performance optimization. When dealing with a large number of async functions, running too many at once can lead to resource exhaustion, while running too few can slow down execution.
The Promise Pool pattern helps control concurrency by limiting the number of Promises that execute simultaneously. In this article, we’ll explore how to implement a Promise Pool in JavaScript, ensuring efficient task execution while maintaining order and preventing overload.
Problem Statement:
Given an array of asynchronous functions (functions
) and a maximum pool size (n
), write an asynchronous function promisePool
that returns a Promise. This Promise should resolve when all functions in the array have completed execution.
The pool size determines the maximum number of Promises that can run concurrently. The promisePool
function should start executing the maximum possible number of functions from the array and initiate new ones as existing Promises complete. Functions must be executed in the order they appear in the array. Once the last Promise resolves, promisePool
should also resolve.
Example:
const functions = [
() => new Promise(res => setTimeout(res, 300)),
() => new Promise(res => setTimeout(res, 400)),
() => new Promise(res => setTimeout(res, 200))
];
const n = 2;
Explanation:
At
t=0
, the first two functions start executing.At
t=300
, the first function completes, and the third function starts.At
t=400
, the second function completes.At
t=500
, the third function completes, and the resulting Promise resolves.
Solution:
The solution involves managing indices and counters to track the execution of functions:
i
: Index of the currently executing function.availPool
: Number of available resources for executing Promises.completedCount
: Number of completed Promises.
If the functions
array is empty, the resulting Promise resolves immediately. Otherwise, a recursive function executeNext
is used to:
Select the next
k
functions, wherek
is the number of available resources.Decrease
availPool
byk
and start executing thesek
functions.Upon completion of each function, increment
availPool
andcompletedCount
.If all functions have completed, resolve the final Promise; otherwise, recursively call
executeNext
.
Here's the implementation:
var promisePool = function(functions, n) {
let i = 0;
let availPool = n;
let completedCount = 0;
return new Promise((resolve) => {
if (functions.length === 0) {
resolve();
return;
}
const executeNext = () => {
const pendingFunctions = functions.slice(i, i + availPool);
i += pendingFunctions.length;
availPool = 0;
pendingFunctions.forEach(func => {
func().then(() => {
availPool++;
completedCount++;
if (completedCount === functions.length) {
resolve();
} else {
executeNext();
}
});
});
};
executeNext();
});
};
Usage Example:
const sleep = (t) => new Promise(res => setTimeout(res, t));
promisePool([() => sleep(500), () => sleep(400)], 1)
.then(() => console.log('All functions have completed.'));
This function ensures that no more than n
Promises are running concurrently, adhering to the specified pool size.