使用ES6的Promise.all()时限制并发的最佳方法是什么?


98

我有一些代码遍历从数据库中查询出来的列表,并对该列表中的每个元素进行HTTP请求。该列表有时可能是一个相当大的数目(成千上万个),并且我想确保我不会遇到具有成千上万个并发HTTP请求的Web服务器。

该代码的缩写版本目前看起来像这样...

function getCounts() {
  return users.map(user => {
    return new Promise(resolve => {
      remoteServer.getCount(user) // makes an HTTP request
      .then(() => {
        /* snip */
        resolve();
      });
    });
  });
}

Promise.all(getCounts()).then(() => { /* snip */});

该代码在节点4.3.2上运行。重申Promise.all一下,是否可以进行管理以便在任何给定时间仅进行一定数量的承诺?



3
不要忘记,这Promise.all确实保证了诺言的进展-诺言是自己做的,Promise.all只是等待它们。
Bergi '16


Answers:


51

请注意Promise.all(),创建诺言本身不会触发诺言开始工作。

考虑到这一点,一种解决方案是检查承诺何时得到解决,是否应该启动新的承诺或您是否已经处于极限。

但是,这里实际上并不需要重新发明轮子。您可以用于此目的的一个库是es6-promise-pool。从他们的例子:

// On the Web, leave out this line and use the script tag above instead. 
var PromisePool = require('es6-promise-pool')

var promiseProducer = function () {
  // Your code goes here. 
  // If there is work left to be done, return the next work item as a promise. 
  // Otherwise, return null to indicate that all promises have been created. 
  // Scroll down for an example. 
}

// The number of promises to process simultaneously. 
var concurrency = 3

// Create a pool. 
var pool = new PromisePool(promiseProducer, concurrency)

// Start the pool. 
var poolPromise = pool.start()

// Wait for the pool to settle. 
poolPromise.then(function () {
  console.log('All promises fulfilled')
}, function (error) {
  console.log('Some promise rejected: ' + error.message)
})

25
不幸的是,es6-promise-pool重新发明了Promise而不是使用它们。我建议改用这种简洁的解决方案(如果您已经在使用ES6或ES7),请访问github.com/rxaviers/async-pool
Rafael Xavier,

3
两者都看,异步池看起来更好!更直接,更轻巧。
无休止的

2
我还发现p-limit是最简单的实现。请参阅下面的示例。stackoverflow.com/a/52262024/8177355
马修·赖德out

2
我认为,微小的asyc池对于限制诺言的并发性要好得多,而且是非侵入性的,而且是很自然的解决方案。
阳光明媚的坦比

73

P限制

我将诺言并发限制与自定义脚本,bluebird,es6-promise-pool和p-limit进行了比较。我相信p-limit具有最简单,最简化的实现方式。请参阅他们的文档

要求

在示例中与异步兼容

我的例子

在此示例中,我们需要为数组中的每个URL运行一个函数(例如,一个API请求)。在此称为fetchData()。如果我们要处理成千上万的项目,那么并发对于节省CPU和内存资源肯定是有用的。

const pLimit = require('p-limit');

// Example Concurrency of 3 promise at once
const limit = pLimit(3);

let urls = [
    "http://www.exampleone.com/",
    "http://www.exampletwo.com/",
    "http://www.examplethree.com/",
    "http://www.examplefour.com/",
]

// Create an array of our promises using map (fetchData() returns a promise)
let promises = urls.map(url => {

    // wrap the function we are calling in the limit function we defined above
    return limit(() => fetchData(url));
});

(async () => {
    // Only three promises are run at once (as defined above)
    const result = await Promise.all(promises);
    console.log(result);
})();

控制台日志结果是您已解析的Promise响应数据的数组。


4
谢谢这个!这是一个简单得多
约翰

3
这是迄今为止我所看到的最好的用于限制同时请求的库。很好的例子,谢谢!
克里斯·利夫达尔

2
感谢您进行比较。您是否将其与github.com/rxaviers/async-pool进行了比较
–'ahong

1
易于使用,绝佳的选择。
drmrbrewer

22

使用 Array.prototype.splice

while (funcs.length) {
  // 100 at at time
  await Promise.all( funcs.splice(0, 100).map(f => f()) )
}

2
这是一个被低估的解决方案。喜欢简单。
布兰农

8
这将成批运行函数而不是池,其中一个函数在另一个函数完成时立即被调用。
cltsang

喜欢这个解决方案!
prasun

花了几秒钟的时间来了解它在做什么,同时又缺乏更多的上下文信息,例如它蜂拥一批而不是一个池。每次从开头或中间进行拼接时,都将对数组重新排序。(浏览器必须重新索引所有项目)理论上更好的替代方法是从头开始取东西,arr.splice(-100)如果订购量不够大,也许您可​​以反转数组:P
Endless

批量运行非常有用。注意:直到当前批次100%完成后,下一个批次才会开始。
Casey Dwayne

20

如果您知道迭代器如何工作以及如何使用迭代器,则不需要任何额外的库,因为自己构建自己的并发变得非常容易。让我示范一下:

/* [Symbol.iterator]() is equivalent to .values()
const iterator = [1,2,3][Symbol.iterator]() */
const iterator = [1,2,3].values()


// loop over all items with for..of
for (const x of iterator) {
  console.log('x:', x)
  
  // notices how this loop continues the same iterator
  // and consumes the rest of the iterator, making the
  // outer loop not logging any more x's
  for (const y of iterator) {
    console.log('y:', y)
  }
}

我们可以使用相同的迭代器,并在工作人员之间共享。

如果您使用.entries().values()是2D[[index, value]]并发,则将获得一个2D数组,我将在下面演示

const sleep = t => new Promise(rs => setTimeout(rs, t))

async function doWork(iterator) {
  for (let [index, item] of iterator) {
    await sleep(1000)
    console.log(index + ': ' + item)
  }
}

const iterator = Array.from('abcdefghij').entries()
const workers = new Array(2).fill(iterator).map(doWork)
//    ^--- starts two workers sharing the same iterator

Promise.allSettled(workers).then(() => console.log('done'))

这样做的好处是您可以拥有一个生成器功能,而不是立即准备好所有功能


注意:与示例async-pool相比,它的不同之处在于它产生了两个工作程序,因此,如果一个工作程序由于某种原因在索引5上抛出错误,它不会阻止其他工作程序执行其余工作。所以,如果做2并发到1去(因此它不会停在那里),所以我的建议是,你赶上了里面所有的错误doWork功能


这太棒了!感谢无尽!
user3413723

这绝对是一个很酷的方法!只要确保您的并发性不超过任务列表的长度(如果您仍然关心结果),那可能会导致额外的费用!
Kris Oye

稍后Streams获得Readable.from(iterator)支持时,可能会更酷。Chrome浏览器已使流可转让。因此您可以创建可读的流并将其发送给Web Worker,而所有这些最终都将使用相同的底层迭代器。
无尽的

16

蓝鸟的Promise.map可以采用并发选项来控制应并行运行多少个Promise。有时,这比.all因为不需要创建promise数组而容易。

const Promise = require('bluebird')

function getCounts() {
  return Promise.map(users, user => {
    return new Promise(resolve => {
      remoteServer.getCount(user) // makes an HTTP request
      .then(() => {
        /* snip */
        resolve();
       });
    });
  }, {concurrency: 10}); // <---- at most 10 http requests at a time
}

如果您需要更快的承诺,bluebird将非常感激,如果您仅将它用于一件事情,它会带来〜18kb的额外垃圾;)
无休止的

1
一切都取决于一件事对您的重要性以及是否有其他更快/更简便的更好方法。一个典型的权衡。我将选择易用性和功能(超过几kb),但选择YMMV。
陈敬韶

11

与其使用promise来限制http请求,不如使用node的内置http.Agent.maxSockets。这消除了使用库或编写自己的池化代码的需求,并具有更多的优势,可以更好地控制您要限制的内容。

agent.maxSockets

默认情况下设置为“无限”。确定代理可以为每个源打开多少个并发套接字。源是“主机:端口”或“主机:端口:本地地址”的组合。

例如:

var http = require('http');
var agent = new http.Agent({maxSockets: 5}); // 5 concurrent connections per origin
var request = http.request({..., agent: agent}, ...);

如果对同一来源发出多个请求,将其设置keepAlive为true也可能会有所帮助(有关更多信息,请参见上面的文档)。


11
仍然,立即创建数千个闭包并池化套接字似乎不是很有效吗?
Bergi

3

我建议库async-pool:https : //github.com/rxaviers/async-pool

npm install tiny-async-pool

描述:

使用本机ES6 / ES7以有限的并发性运行多个承诺返回和异步功能

asyncPool在有限的并发池中运行多个承诺返回和异步功能。只要其中一项承诺被拒绝,它就会立即拒绝。当所有诺言完成时,它就会解决。它会尽快(在并发限制下)调用迭代器函数。

用法:

const timeout = i => new Promise(resolve => setTimeout(() => resolve(i), i));
await asyncPool(2, [1000, 5000, 3000, 2000], timeout);
// Call iterator (i = 1000)
// Call iterator (i = 5000)
// Pool limit of 2 reached, wait for the quicker one to complete...
// 1000 finishes
// Call iterator (i = 3000)
// Pool limit of 2 reached, wait for the quicker one to complete...
// 3000 finishes
// Call iterator (i = 2000)
// Itaration is complete, wait until running ones complete...
// 5000 finishes
// 2000 finishes
// Resolves, results are passed in given array order `[1000, 5000, 3000, 2000]`.

1
为我工作。谢谢。这是一个很棒的图书馆。
阳光明媚的坦比

2

可以使用递归来解决。

这个想法是,最初,您发送的请求数量将达到允许的最大数量,并且这些请求中的每一个都应在完成后递归地继续发送自己。

function batchFetch(urls, concurrentRequestsLimit) {
    return new Promise(resolve => {
        var documents = [];
        var index = 0;

        function recursiveFetch() {
            if (index === urls.length) {
                return;
            }
            fetch(urls[index++]).then(r => {
                documents.push(r.text());
                if (documents.length === urls.length) {
                    resolve(documents);
                } else {
                    recursiveFetch();
                }
            });
        }

        for (var i = 0; i < concurrentRequestsLimit; i++) {
            recursiveFetch();
        }
    });
}

var sources = [
    'http://www.example_1.com/',
    'http://www.example_2.com/',
    'http://www.example_3.com/',
    ...
    'http://www.example_100.com/'
];
batchFetch(sources, 5).then(documents => {
   console.log(documents);
});

2

这是我的ES7解决方案,适用于复制粘贴友好且功能完整Promise.all()/map()替代,并发限制。

类似于Promise.all()它维护退货顺序以及非承诺退货值的回退。

我还对不同实现进行了比较,因为它说明了其他一些解决方案遗漏的某些方面。

用法

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

实作

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();
  });
}

比较方式

// 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

结论

asyncPool() 应该是最好的解决方案,因为它允许新请求在任何先前请求完成后立即开始。

asyncBatch() 包含在内是作为比较,因为它的实现较容易理解,但是它的性能应较慢,因为需要完成同一批处理中的所有请求才能启动下一个批处理。

在这个人为的例子中,无限制的香草Promise.all()当然是最快的,而其他香草在现实的拥挤场景中可能表现得更理想。

更新资料

其他人已经建议的async-pool库可能是我的实现的更好替代方案,因为它几乎相同地工作,并且通过巧妙地使用Promise.race()具有更简洁的实现:https : //github.com/rxaviers/异步池/blob/master/lib/es7.js

希望我的回答仍然可以提供教育价值。


1

这里是流和“ p-limit”的基本示例。它将HTTP读取流传输到mongo db。

const stream = require('stream');
const util = require('util');
const pLimit = require('p-limit');
const es = require('event-stream');
const streamToMongoDB = require('stream-to-mongo-db').streamToMongoDB;


const pipeline = util.promisify(stream.pipeline)

const outputDBConfig = {
    dbURL: 'yr-db-url',
    collection: 'some-collection'
};
const limit = pLimit(3);

async yrAsyncStreamingFunction(readStream) => {
        const mongoWriteStream = streamToMongoDB(outputDBConfig);
        const mapperStream = es.map((data, done) => {
                let someDataPromise = limit(() => yr_async_call_to_somewhere())

                    someDataPromise.then(
                        function handleResolve(someData) {

                            data.someData = someData;    
                            done(null, data);
                        },
                        function handleError(error) {
                            done(error)
                        }
                    );
                })

            await pipeline(
                readStream,
                JSONStream.parse('*'),
                mapperStream,
                mongoWriteStream
            );
        }

0

因此,我尝试使显示的示例适用于我的代码,但是由于这仅适用于导入脚本,而不适用于生产代码,因此使用npm包batch-promises无疑是我最简单的方法

注意:需要运行时以支持Promise或被填充。

Api batchPromises(int:batchSize,array:Collection,i => Promise:Iteratee)在每个批处理之后将调用Promise:Iteratee。

使用:

batch-promises
Easily batch promises

NOTE: Requires runtime to support Promise or to be polyfilled.

Api
batchPromises(int: batchSize, array: Collection, i => Promise: Iteratee)
The Promise: Iteratee will be called after each batch.

Use:
import batchPromises from 'batch-promises';
 
batchPromises(2, [1,2,3,4,5], i => new Promise((resolve, reject) => {
 
  // The iteratee will fire after each batch resulting in the following behaviour:
  // @ 100ms resolve items 1 and 2 (first batch of 2)
  // @ 200ms resolve items 3 and 4 (second batch of 2)
  // @ 300ms resolve remaining item 5 (last remaining batch)
  setTimeout(() => {
    resolve(i);
  }, 100);
}))
.then(results => {
  console.log(results); // [1,2,3,4,5]
});


0

如果您不想使用外部库,那么递归就是答案

downloadAll(someArrayWithData){
  var self = this;

  var tracker = function(next){
    return self.someExpensiveRequest(someArrayWithData[next])
    .then(function(){
      next++;//This updates the next in the tracker function parameter
      if(next < someArrayWithData.length){//Did I finish processing all my data?
        return tracker(next);//Go to the next promise
      }
    });
  }

  return tracker(0); 
}

0

这是我Promise.race在这里使用的代码

const identifyTransactions = async function() {
  let promises = []
  let concurrency = 0
  for (let tx of this.transactions) {
    if (concurrency > 4)
      await Promise.race(promises).then(r => { promises = []; concurrency = 0 })
    promises.push(tx.identifyTransaction())
    concurrency++
  }
  if (promises.length > 0)
    await Promise.race(promises) //resolve the rest
}

如果您想查看示例:https : //jsfiddle.net/thecodermarcelo/av2tp83o/5/


2
我不会称之为并发...这更像是批处理执行...您执行4个任务,等待所有任务完成,然后执行下一个4。如果其中一个解决得早,您仍然要等待其他3个任务完成,您应该使用的是Promise.race
无止境的


0
  • @tcooc的答案很酷。对此一无所知,将来会利用它。
  • 我也很喜欢@MatthewRideout的答案,但是它使用了一个外部库!

只要有可能,我都会自行开发这种东西,而不是去图书馆。您最终学习了很多以前似乎令人生畏的概念。

你们如何看待这种尝试:(
我考虑了很多,而且我认为它是可行的,但要指出是不是还是根本上有错误)

 class Pool{
        constructor(maxAsync) {
            this.maxAsync = maxAsync;
            this.asyncOperationsQueue = [];
            this.currentAsyncOperations = 0
        }

        runAnother() {
            if (this.asyncOperationsQueue.length > 0 && this.currentAsyncOperations < this.maxAsync) {
                this.currentAsyncOperations += 1;
                this.asyncOperationsQueue.pop()()
                    .then(() => { this.currentAsyncOperations -= 1; this.runAnother() }, () => { this.currentAsyncOperations -= 1; this.runAnother() })
            }
        }

        add(f){  // the argument f is a function of signature () => Promise
            this.runAnother();
            return new Promise((resolve, reject) => {
                this.asyncOperationsQueue.push(
                    () => f().then(resolve).catch(reject)
                )
            })
        }
    }

//#######################################################
//                        TESTS
//#######################################################

function dbCall(id, timeout, fail) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (fail) {
               reject(`Error for id ${id}`);
            } else {
                resolve(id);
            }
        }, timeout)
    }
    )
}


const dbQuery1 = () => dbCall(1, 5000, false);
const dbQuery2 = () => dbCall(2, 5000, false);
const dbQuery3 = () => dbCall(3, 5000, false);
const dbQuery4 = () => dbCall(4, 5000, true);
const dbQuery5 = () => dbCall(5, 5000, false);


const cappedPool = new Pool(2);

const dbQuery1Res = cappedPool.add(dbQuery1).catch(i => i).then(i => console.log(`Resolved: ${i}`))
const dbQuery2Res = cappedPool.add(dbQuery2).catch(i => i).then(i => console.log(`Resolved: ${i}`))
const dbQuery3Res = cappedPool.add(dbQuery3).catch(i => i).then(i => console.log(`Resolved: ${i}`))
const dbQuery4Res = cappedPool.add(dbQuery4).catch(i => i).then(i => console.log(`Resolved: ${i}`))
const dbQuery5Res = cappedPool.add(dbQuery5).catch(i => i).then(i => console.log(`Resolved: ${i}`))

这种方法提供了一个不错的API,类似于scala / java中的线程池。
用创建一个池实例后const cappedPool = new Pool(2),您可以简单地向它提供承诺cappedPool.add(() => myPromise)
显然,我们必须确保承诺不会立即开始,这就是为什么我们必须在职能的帮助下“懒惰地提供”。

最重要的是,请注意,该方法的结果add 是一个Promise,它将以您最初的诺言的价值完成/解决!这使得使用非常直观。

const resultPromise = cappedPool.add( () => dbCall(...))
resultPromise
.then( actualResult => {
   // Do something with the result form the DB
  }
)

0

不幸的是,原生Promise.all无法做到这一点,因此您必须发挥创造力。

这是我无需使用任何外部库就可以找到的最快,最简洁的方法。

它利用了称为迭代器的较新的javascript功能。迭代器基本上跟踪已处理的项目和未处理的项目。

为了在代码中使用它,您创建了一个异步函数数组。每个异步函数都向同一迭代器询问需要处理的下一项。每个函数异步处理自己的项目,完成后向迭代器询问新的项目。一旦迭代器的项目用完,所有功能将完成。

感谢@Endless的启发。

var items = [
    "https://www.stackoverflow.com",
    "https://www.stackoverflow.com",
    "https://www.stackoverflow.com",
    "https://www.stackoverflow.com",
    "https://www.stackoverflow.com",
    "https://www.stackoverflow.com",
    "https://www.stackoverflow.com",
    "https://www.stackoverflow.com",
];

var concurrency = 5

Array(concurrency).fill(items.entries()).map(async (cursor) => {
    for(let [index, url] of cursor){
        console.log("getting url is ", index, url);
        // run your async task instead of this next line
        var text = await fetch(url).then(res => res.text());
        console.log("text is", text.slice(0,20));
    }
})


好奇为什么这个记下来了。这与我想出的非常相似。
Kris Oye

0

这么多好的解决方案。我从@Endless发布的优雅解决方案开始,最后得到了这个小的扩展方法,该方法不使用任何外部库,也不批量运行(尽管假定您具有异步等功能):

Promise.allWithLimit = async (taskList, limit = 5) => {
    const iterator = taskList.entries();
    let results = new Array(taskList.length);
    let workerThreads = new Array(limit).fill(0).map(() => 
        new Promise(async (resolve, reject) => {
            try {
                let entry = iterator.next();
                while (!entry.done) {
                    let [index, promise] = entry.value;
                    try {
                        results[index] = await promise;
                        entry = iterator.next();
                    }
                    catch (err) {
                        results[index] = err;
                    }
                }
                // No more work to do
                resolve(true); 
            }
            catch (err) {
                // This worker is dead
                reject(err);
            }
        }));

    await Promise.all(workerThreads);
    return results;
};

    Promise.allWithLimit = async (taskList, limit = 5) => {
        const iterator = taskList.entries();
        let results = new Array(taskList.length);
        let workerThreads = new Array(limit).fill(0).map(() => 
            new Promise(async (resolve, reject) => {
                try {
                    let entry = iterator.next();
                    while (!entry.done) {
                        let [index, promise] = entry.value;
                        try {
                            results[index] = await promise;
                            entry = iterator.next();
                        }
                        catch (err) {
                            results[index] = err;
                        }
                    }
                    // No more work to do
                    resolve(true); 
                }
                catch (err) {
                    // This worker is dead
                    reject(err);
                }
            }));
    
        await Promise.all(workerThreads);
        return results;
    };

    const demoTasks = new Array(10).fill(0).map((v,i) => new Promise(resolve => {
       let n = (i + 1) * 5;
       setTimeout(() => {
          console.log(`Did nothing for ${n} seconds`);
          resolve(n);
       }, n * 1000);
    }));

    var results = Promise.allWithLimit(demoTasks);

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.