background

Asynchrony is a very important feature of JS, but most of the time, we want to not only have a series of tasks executed in parallel, but also control the number of concurrent executions, especially in asynchronous tasks that operate on limited resources, such as file handles, network ports, etc.

Let’s look at an example.

function sleep(ms) {
  return new Promise(resolve= > setTimeout(resolve, ms));
}

// simulate an async work that takes 1s to finish
async function execute(id) {
  console.log(`start work ${id}`);
  await sleep(1000);
  console.log(`work ${id} done`);
}

Promise.all([1.2.3.4.5.6.7.8.9].map(execute));
Copy the code

Output result:

"start work 1" "start work 2" "start work 3" "start work 4" "start work 5" "start work 6" "start work 7" "start work 8" "start work 9" "work 1 done" "work 2 done" "work 3 done" "work 4 done" "work 5 done" "work 6 done" "work 7 done" "work 8  done" "work 9 done"Copy the code

As you can see, all the work starts executing at the same time.

Now, what if we want to execute only 2 of these works at a time, and then proceed to the next 2 with a concurrent number of 2?

The solution

controlPromiseThe generation of is key

As we know, promise.all does not trigger the execution of a Promise. What triggers the execution is the creation of the Promise itself. In other words, the Promise is executed as soon as it is made! Therefore, if we want to control the concurrency of promises, we need to control the generation of promises.

throughIteratorControl concurrency

The common solution is to receive the concurrent task array through a function, the concurrent function, the number of concurrent parameters, according to the number of concurrent, monitor the completion state of the Promise, batch create new promises, so as to achieve the purpose of controlling the generation of promises.

Now, let’s try another idea, which is to use the Iterator to control concurrency.

I’m going to iterate over the same thingIteratorWhat happens?

Let’s look at a simplified example first.

// Array.values returns an Array Iterator
const iterator = [1.2.3].values();

for (const x of iterator) {
  console.log(`loop x: ${x}`);

  for (const y of iterator) {
    console.log(`loop y: ${y}`); }}Copy the code

Output result:

"loop x: 1"
"loop y: 2"
"loop y: 3"
Copy the code

Notice that? The y loop continues with the X loop, and both loops end after all the elements are iterated! That’s what we’re going to take advantage of. For those unfamiliar with Iterator, see the MDN article: developer.mozilla.org/en-US/docs/…

withIteratorExample of transforming work

Let’s use this feature of Iterator to modify our original work example.

// generate workers according to concurrency number
// each worker takes the same iterator
const limit = concurrency= > iterator= > {
  const workers = new Array(concurrency);
  return workers.fill(iterator);
};

// run tasks in an iterator one by one
const run = func= > async iterator => {
  for (const item of iterator) {
    awaitfunc(item); }};// wrap limit and run together
function asyncTasks(array, func, concurrency = 1) {
  return limit(concurrency)(array.values()).map(run(func));
}

Promise.all(asyncTasks(tasks, execute, 2));
Copy the code

Output result:

"start work 1"
"start work 2"
"work 1 done"
"start work 3"
"work 2 done"
"start work 4"
"work 3 done"
"start work 5"
"work 4 done"
"start work 6"
"work 5 done"
"start work 7"
"work 6 done"
"start work 8"
"work 7 done"
"start work 9"
"work 8 done"
"work 9 done"
Copy the code

As expected, only two asynchronous tasks are executed at a time until all tasks are completed.

Still, the plan isn’t perfect. The main problem is that if one worker makes a mistake during execution, other workers will not stop working. In other words, in the above example, if worker 1 stops abnormally, worker 2 will perform all remaining tasks alone until all tasks are completed. Therefore, if you want to maintain two concurrent sessions at all times, the easiest way is to add a catch to each execute method.

While not perfect, creating Iterator as a control Promise is a simple and effective way to control the number of asynchronous concurrency.

Of course, in practical projects, we should try to avoid repeating the wheel. P-limit, Async-Pool and even Bluebird are all simple and easy to use solutions.