What is the best way to limit concurrency when using ES6's Promise.all()?

前端 未结 17 788
执念已碎
执念已碎 2020-11-29 21:28

I have some code that is iterating over a list that was queried out of a database and making an HTTP request for each element in that list. That list can sometimes be a rea

17条回答
  •  醉话见心
    2020-11-29 21:45

    Here is my ES7 solution to a copy-paste friendly and feature complete Promise.all()/map() alternative, with a concurrency limit.

    Similar to Promise.all() it maintains return order as well as a fallback for non promise return values.

    I also included a comparison of the different implementation as it illustrates some aspects a few of the other solutions have missed.

    Usage

    const asyncFn = delay => new Promise(resolve => setTimeout(() => resolve(), delay));
    const args = [30, 20, 15, 10];
    await asyncPool(args, arg => asyncFn(arg), 4); // concurrency limit of 4
    

    Implementation

    async function asyncBatch(args, fn, limit = 8) {
      // Copy arguments to avoid side effects
      args = [...args];
      const outs = [];
      while (args.length) {
        const batch = args.splice(0, limit);
        const out = await Promise.all(batch.map(fn));
        outs.push(...out);
      }
      return outs;
    }
    
    async function asyncPool(args, fn, limit = 8) {
      return new Promise((resolve) => {
        // Copy arguments to avoid side effect, reverse queue as
        // pop is faster than shift
        const argQueue = [...args].reverse();
        let count = 0;
        const outs = [];
        const pollNext = () => {
          if (argQueue.length === 0 && count === 0) {
            resolve(outs);
          } else {
            while (count < limit && argQueue.length) {
              const index = args.length - argQueue.length;
              const arg = argQueue.pop();
              count += 1;
              const out = fn(arg);
              const processOut = (out, index) => {
                outs[index] = out;
                count -= 1;
                pollNext();
              };
              if (typeof out === 'object' && out.then) {
                out.then(out => processOut(out, index));
              } else {
                processOut(out, index);
              }
            }
          }
        };
        pollNext();
      });
    }
    

    Comparison

    // A simple async function that returns after the given delay
    // and prints its value to allow us to determine the response order
    const asyncFn = delay => new Promise(resolve => setTimeout(() => {
      console.log(delay);
      resolve(delay);
    }, delay));
    
    // List of arguments to the asyncFn function
    const args = [30, 20, 15, 10];
    
    // As a comparison of the different implementations, a low concurrency
    // limit of 2 is used in order to highlight the performance differences.
    // If a limit greater than or equal to args.length is used the results
    // would be identical.
    
    // Vanilla Promise.all/map combo
    const out1 = await Promise.all(args.map(arg => asyncFn(arg)));
    // prints: 10, 15, 20, 30
    // total time: 30ms
    
    // Pooled implementation
    const out2 = await asyncPool(args, arg => asyncFn(arg), 2);
    // prints: 20, 30, 15, 10
    // total time: 40ms
    
    // Batched implementation
    const out3 = await asyncBatch(args, arg => asyncFn(arg), 2);
    // prints: 20, 30, 20, 30
    // total time: 45ms
    
    console.log(out1, out2, out3); // prints: [30, 20, 15, 10] x 3
    
    // Conclusion: Execution order and performance is different,
    // but return order is still identical
    

    Conclusion

    asyncPool() should be the best solution as it allows new requests to start as soon as any previous one finishes.

    asyncBatch() is included as a comparison as its implementation is simpler to understand, but it should be slower in performance as all requests in the same batch is required to finish in order to start the next batch.

    In this contrived example, the non-limited vanilla Promise.all() is of course the fastest, while the others could perform more desirable in a real world congestion scenario.

    Update

    The async-pool library that others have already suggested is probably a better alternative to my implementation as it works almost identically and has a more concise implementation with a clever usage of Promise.race(): https://github.com/rxaviers/async-pool/blob/master/lib/es7.js

    Hopefully my answer can still serve an educational value.

提交回复
热议问题