繁体   English   中英

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

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

我有一些代码迭代从数据库中查询出的列表,并对该列表中的每个元素发出 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 */});

此代码在 Node 4.3.2 上运行。 重申一下,是否可以管理Promise.all以便在任何给定时间只有一定数量的 Promise 在进行中?

P-极限

我将 promise 并发限制与自定义脚本、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);
})();

控制台日志结果是您已解决的承诺响应数据的数组。

使用Array.prototype.splice

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

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

考虑到这一点,一种解决方案是在解决承诺时检查是否应该开始新的承诺,或者您是否已经达到极限。

但是,这里真的没有必要重新发明轮子。 您可以为此目的使用的一个库是es6-promise-pool 从他们的例子中:

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

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

 /* [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()您将获得一个带有[[index, value]]的二维数组,我将在下面以 2 的并发性进行演示

 const sleep = t => new Promise(rs => setTimeout(rs, t)) const iterator = Array.from('abcdefghij').entries() // const results = [] || Array(someLength) async function doWork (iterator) { for (let [index, item] of iterator) { await sleep(1000) console.log(index + ': ' + item) // in case you need to store the results in order // results[index] = item + item // or if the order dose not mather // results.push(item + item) } } const workers = Array(2).fill(iterator).map(doWork) // ^--- starts two workers sharing the same iterator Promise.allSettled(workers).then(console.log.bind(null, 'done'))

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

更棒的是,您可以在 node 中执行stream.Readable.from(iterator) (最终也可以在 whatwg 流中)。 并且使用可转移的 ReadbleStream,如果您正在与网络工作者一起工作以进行表演,这使得该功能在该功能中非常有用


注意:与示例 异步池相比,与此不同的是它产生了两个工作人员,因此如果一个工作人员由于某种原因在索引 5 处抛出错误,它不会阻止另一个工作人员完成其余工作。 所以你从 2 并发到 1。(所以它不会停在那里)所以我的建议是你在doWork函数中捕获所有错误

不要使用 promises 来限制 http 请求,而是使用 node 的内置http.Agent.maxSockets 这消除了使用库或编写自己的池代码的要求,并具有额外的优势,可以更好地控制您所限制的内容。

agent.maxSockets

默认设置为无穷大。 确定代理可以为每个源打开多少个并发套接字。 Origin 是“host:port”或“host:port:localAddress”组合。

例如:

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

如果向同一个来源发出多个请求,将keepAlive设置为 true 也可能使您受益(有关更多信息,请参阅上面的文档)。

bluebird 的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
}

正如此答案线程中的所有其他人所指出的那样,如果您需要限制并发性, Promise.all()将不会做正确的事情。 但理想情况下,您甚至应该等到所有Promise 都完成后再处理它们。

相反,您希望在每个结果可用时尽快处理它,因此您不必等待最后一个 Promise 完成后再开始迭代它们。

所以,这是一个代码示例,它部分基于Endless 的答案以及 TJ Crowder 的这个答案

 // example tasks that sleep and return a number // in real life, you'd probably fetch URLs or something const tasks = []; for (let i = 0; i < 20; i++) { tasks.push(async () => { console.log(`start ${i}`); await sleep(Math.random() * 1000); console.log(`end ${i}`); return i; }); } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } (async () => { for await (let value of runTasks(3, tasks.values())) { console.log(`output ${value}`); } })(); async function* runTasks(maxConcurrency, taskIterator) { // Each async iterator is a worker, polling for tasks from the shared taskIterator // Sharing the iterator ensures that each worker gets unique tasks. const asyncIterators = new Array(maxConcurrency); for (let i = 0; i < maxConcurrency; i++) { asyncIterators[i] = (async function* () { for (const task of taskIterator) yield await task(); })(); } yield* raceAsyncIterators(asyncIterators); } async function* raceAsyncIterators(asyncIterators) { async function nextResultWithItsIterator(iterator) { return { result: await iterator.next(), iterator: iterator }; } /** @type Map<AsyncIterator<T>, Promise<{result: IteratorResult<T>, iterator: AsyncIterator<T>}>> */ const promises = new Map(asyncIterators.map(iterator => [iterator, nextResultWithItsIterator(iterator)])); while (promises.size) { const { result, iterator } = await Promise.race(promises.values()); if (result.done) { promises.delete(iterator); } else { promises.set(iterator, nextResultWithItsIterator(iterator)); yield result.value; } } }

这里有很多魔法; 让我解释。

这个解决方案是围绕异步生成器函数构建的,许多 JS 开发人员可能并不熟悉。

生成器函数(又名function*函数)返回“生成器”,即结果的迭代器。 生成器函数可以在通常使用return关键字的地方使用yield关键字。 当调用者第一次在生成器上调用next()时(或使用for...of循环), function*函数将一直运行,直到它yield sa 值; 这成为迭代器的next()值。 但随后调用next()时,生成器函数从yield语句恢复,就在它停止的地方,即使它在循环的中间。 (您也可以yield* ,以产生另一个生成器函数的所有结果。)

“异步生成器函数”( async function* )是一个生成器函数,它返回一个“异步迭代器”,它是一个 Promise 的迭代器。 您可以在异步迭代器上调用for await...of 异步生成器函数可以使用await关键字,就像在任何async function中一样。

在示例中,我们使用一组任务函数调用runTasks() runTasks()是一个异步生成器函数,因此我们可以使用for await...of循环调用它。 每次循环运行时,我们都会处理最新完成的任务的结果。

runTasks()创建 N 个异步迭代器,即工人。 (请注意,worker 最初被定义为异步生成器函数,但我们立即调用每个函数,并将每个生成的异步迭代器存储在asyncIterators数组中。)该示例调用具有 3 个并发 worker 的runTasks ,因此启动的任务不超过 3 个同时。 当任何任务完成时,我们立即将下一个任务排队。 (这优于“批处理”,您一次执行 3 个任务,等待所有三个任务,并且在整个前一批完成之前不要开始下一批三个。)

runTasks()通过使用yield* raceAsyncIterators() “竞速”它的异步迭代器来结束。 raceAsyncIterators()类似于Promise.race()但它竞争 N 个 Promises 迭代器,而不仅仅是 N 个 Promises; 它返回一个异步迭代器,该迭代器产生已解决的 Promise 的结果。

raceAsyncIterators()首先定义一个从每个迭代器到 Promise 的promises Map 每个承诺都是对迭代结果的承诺以及生成它的迭代器。

使用promises映射,我们可以Promise.race()映射的值,为我们提供获胜的迭代结果及其迭代器。 如果迭代器完全done ,我们将其从地图中移除; 否则,我们用迭代器的next() Promise 和yield result.value替换promises映射中的 Promise。

总之, runTasks()是一个异步生成器函数,它产生 N 个并发异步任务迭代器的结果,因此最终用户可以只for await (let value of runTasks(3, tasks.values()))来处理每个结果一旦可用。

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

npm install tiny-async-pool

描述:

使用原生 ES6/ES7 以有限的并发运行多个 promise-returning & async 函数

asyncPool 在有限的并发池中运行多个 promise-returning & async 函数。 一旦其中一个承诺被拒绝,它就会立即拒绝。 它在所有承诺完成时解决。 它尽快(在并发限制下)调用迭代器函数。

用法:

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]`.

这是我的 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()作为比较,因为它的实现更易于理解,但它的性能应该更慢,因为同一批次中的所有请求都需要完成才能开始下一批。

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

更新

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

希望我的回答仍然具有教育价值。

信号量是众所周知的并发原语,旨在解决类似问题。 它是非常通用的构造,信号量的实现存在于多种语言中。 这就是使用 Semaphore 解决此问题的方式:

async function main() {
  const s = new Semaphore(100);
  const res = await Promise.all(
    entities.map((users) => 
      s.runExclusive(() => remoteServer.getCount(user))
    )
  );
  return res;
}

我正在使用来自async-mutex的 Semaphore 实现,它有不错的文档和 TypeScript 支持。

如果您想深入研究此类主题,可以查看“信号量小书”一书,该书可在此处以 PDF 格式免费获得

这是流式传输和“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
            );
        }

可以使用递归来解决。

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

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);
});
  • @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 ,它将使用您原始 promise 的值完成/解决 这使得使用非常直观。

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

下面的concurrent function 将返回一个 Promise,它解析为一个已解析 promise 值的数组,同时实现并发限制。 没有第 3 方库。

 // waits 50 ms then resolves to the passed-in arg const sleepAndResolve = s => new Promise(rs => setTimeout(()=>rs(s), 50)) // queue 100 promises const funcs = [] for(let i=0; i<100; i++) funcs.push(()=>sleepAndResolve(i)) //run the promises with a max concurrency of 10 concurrent(10,funcs).then(console.log) // prints [0,1,2...,99].catch(()=>console.log("there was an error")) /** * Run concurrent promises with a maximum concurrency level * @param concurrency The number of concurrently running promises * @param funcs An array of functions that return promises * @returns a promise that resolves to an array of the resolved values from the promises returned by funcs */ function concurrent(concurrency, funcs) { return new Promise((resolve, reject) => { let index = -1; const p = []; for (let i = 0; i < Math.max(1, Math.min(concurrency, funcs.length)); i++) runPromise(); function runPromise() { if (++index < funcs.length) (p[p.length] = funcs[index]()).then(runPromise).catch(reject); else if (index === funcs.length) Promise.all(p).then(resolve).catch(reject); } }); }

如果您有兴趣,这里是 Typescript 版本

/**
 * Run concurrent promises with a maximum concurrency level
 * @param concurrency The number of concurrently running promises
 * @param funcs An array of functions that return promises
 * @returns a promise that resolves to an array of the resolved values from the promises returned by funcs
 */
function concurrent<V>(concurrency:number, funcs:(()=>Promise<V>)[]):Promise<V[]> {
  return new Promise((resolve,reject)=>{
    let index = -1;
    const p:Promise<V>[] = []
    for(let i=0; i<Math.max(1,Math.min(concurrency, funcs.length)); i++) runPromise()
    function runPromise() {
      if (++index < funcs.length) (p[p.length] = funcs[index]()).then(runPromise).catch(reject)
      else if (index === funcs.length) Promise.all(p).then(resolve).catch(reject)
    }
  })
}

因此,我尝试为我的代码制作一些示例,但由于这仅适用于导入脚本而不是生产代码,因此使用 npm 包batch-promises对我来说肯定是最简单的路径

注意:需要运行时来支持 Promise 或被 polyfill。

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

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

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

不幸的是,原生 Promise.all 无法做到这一点,所以你必须要有创意。

这是我在不使用任何外部库的情况下能找到的最快最简洁的方法。

它利用了一个新的 JavaScript 特性,称为迭代器。 迭代器基本上跟踪哪些项目已处理,哪些尚未处理。

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

感谢@Endless 的启发。

 const items = [ 'https://httpbin.org/bytes/2', 'https://httpbin.org/bytes/2', 'https://httpbin.org/bytes/2', 'https://httpbin.org/bytes/2', 'https://httpbin.org/bytes/2', 'https://httpbin.org/bytes/2', 'https://httpbin.org/bytes/2', 'https://httpbin.org/bytes/2' ] const 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)) } })

这么多好的解决方案。 我从@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);

扩展@deceleratedcaviar 发布的答案,我创建了一个“批处理”实用函数,它以参数为参数:值数组、并发限制和处理函数。 是的,我意识到使用 Promise.all 这种方式更类似于批处理与真正的并发性,但如果目标是一次限制过多的 HTTP 调用,我会选择这种方法,因为它简单且不需要外部库.

 async function batch(o) { let arr = o.arr let resp = [] while (arr.length) { let subset = arr.splice(0, o.limit) let results = await Promise.all(subset.map(o.process)) resp.push(results) } return [].concat.apply([], resp) } let arr = [] for (let i = 0; i < 250; i++) { arr.push(i) } async function calc(val) { return val * 100 } (async () => { let resp = await batch({ arr: arr, limit: 100, process: calc }) console.log(resp) })();

使用自定义承诺库 ( CPromise ) 的另一种解决方案:

    import { CPromise } from "c-promise2";
    import cpFetch from "cp-fetch";
    
    const promise = CPromise.all(
      function* () {
        const urls = [
          "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s&x=1",
          "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s&x=2",
          "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s&x=3",
          "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s&x=4",
          "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s&x=5",
          "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s&x=6",
          "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s&x=7"
        ];
    
        for (const url of urls) {
          yield cpFetch(url); // add a promise to the pool
          console.log(`Request [${url}] completed`);
        }
      },
      { concurrency: 2 }
    ).then(
      (v) => console.log(`Done: `, v),
      (e) => console.warn(`Failed: ${e}`)
    );
    
    // yeah, we able to cancel the task and abort pending network requests
    // setTimeout(() => promise.cancel(), 4500);

    import { CPromise } from "c-promise2";
    import cpFetch from "cp-fetch";
    
    const promise = CPromise.all(
      [
        "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s&x=1",
        "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s&x=2",
        "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s&x=3",
        "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s&x=4",
        "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s&x=5",
        "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s&x=6",
        "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s&x=7"
      ],
      {
        mapper: (url) => {
          console.log(`Request [${url}]`);
          return cpFetch(url);
        },
        concurrency: 2
      }
    ).then(
      (v) => console.log(`Done: `, v),
      (e) => console.warn(`Failed: ${e}`)
    );
    
    // yeah, we able to cancel the task and abort pending network requests
    //setTimeout(() => promise.cancel(), 4500);

警告这还没有以效率为基准,并且做了很多数组复制/创建

如果您想要更实用的方法,您可以执行以下操作:

import chunk from 'lodash.chunk';

const maxConcurrency = (max) => (dataArr, promiseFn) =>
  chunk(dataArr, max).reduce(
      async (agg, batch) => [
          ...(await agg),
          ...(await Promise.all(batch.map(promiseFn)))
      ],
      []
  );

然后你可以像这样使用它:

const randomFn = (data) =>
    new Promise((res) => setTimeout(
      () => res(data + 1),
        Math.random() * 1000
      ));


const result = await maxConcurrency(5)(
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
    randomFn
);
console.log('result+++', result);

我一直在使用瓶颈库,我真的很喜欢它,但在我的情况下,它没有释放内存并一直在处理长时间运行的作业......这对于运行你可能想要节流/并发的大量作业并不好首先是图书馆。

我需要一个简单、开销低、易于维护的解决方案。 我还想要让池保持充值的东西,而不是简单地批处理预定义的块......对于下载器,这将阻止n GB 文件一次阻止你的队列几分钟/小时,即使其余的批次在很久以前就完成了。

这是我一直在使用的 Node.js v16+、无依赖、异步生成器解决方案:

 const promiseState = function( promise ) { // A promise could never resolve to a unique symbol unless it was in this scope const control = Symbol(); // This helps us determine the state of the promise... A little heavy, but it beats a third-party promise library. The control is the second element passed to Promise.race() since it will only resolve first if the promise being tested is pending. return Promise .race([ promise, control ]) .then( value => ( value === control ) ? 'pending' : 'fulfilled' ) .catch( () => 'rejected' ); } const throttle = async function* ( reservoir, promiseFunction, highWaterMark ) { let iterable = reservoir.splice( 0, highWaterMark ).map( item => promiseFunction( item ) ); while ( iterable.length > 0 ) { // When a promise has resolved we have space to top it up to the high water mark... await Promise.any( iterable ); const pending = []; const resolved = []; // This identifies the promise(s) that have resolved so that we can yield them for ( const currentValue of iterable ) { if ( await promiseState( currentValue ) === 'pending' ) { pending.push( currentValue ); } else { resolved.push( currentValue ); } } // Put the remaining promises back into iterable, and top it to the high water mark iterable = [ ...pending, ...reservoir.splice( 0, highWaterMark - pending.length ).map( value => promiseFunction( value ) ) ]; yield Promise.allSettled( resolved ); } } // This is just an example of what would get passed as "promiseFunction"... This can be the function that returns your HTTP request promises const getTimeout = delay => new Promise( (resolve, reject) => setTimeout(resolve, delay, delay) ); // This is just the async IIFE that bootstraps this example ( async () => { const test = [ 1000, 2000, 3000, 4000, 5000, 6000, 1500, 2500, 3500, 4500, 5500, 6500 ]; for await ( const timeout of throttle( test, getTimeout, 4 ) ) { console.log( timeout ); } } )();

我有创建块并使用 .reduce 函数等待每个块 promise.alls 完成的解决方案。 如果承诺有一些调用限制,我也会添加一些延迟。

export function delay(ms: number) {
  return new Promise<void>((resolve) => setTimeout(resolve, ms));
}

export const chunk = <T>(arr: T[], size: number): T[][] => [
  ...Array(Math.ceil(arr.length / size)),
].map((_, i) => arr.slice(size * i, size + size * i));

const myIdlist = []; // all items
const groupedIdList = chunk(myIdList, 20); // grouped by 20 items

await groupedIdList.reduce(async (prev, subIdList) => {
  await prev;
  // Make sure we wait for 500 ms after processing every page to prevent overloading the calls.
  const data = await Promise.all(subIdList.map(myPromise));
  await delay(500);
}, Promise.resolve());

这个解决方案使用异步生成器来管理带有 vanilla javascript 的并发承诺。 throttle生成器有 3 个参数:

  • 要作为参数提供给 Promise 生成函数的值数组。 (例如,一组 URL。)
  • 一个返回承诺的函数。 (例如,返回一个 HTTP 请求的承诺。)
  • 一个整数,表示允许的最大并发承诺。

Promise 仅在需要时实例化,以减少内存消耗。 可以使用for await...of语句迭代结果。

下面的示例提供了一个检查 promise 状态的函数、throttle 异步生成器,以及一个基于setTimeout返回 promise 的简单函数。 最后的async IIFE定义了超时值的存储库,设置了throttle返回的异步迭代,然后在结果解析时对其进行迭代。

如果您想要更完整的 HTTP 请求示例,请在评论中告诉我。

请注意,异步生成器需要 Node.js 16+

 const promiseState = function( promise ) { const control = Symbol(); return Promise .race([ promise, control ]) .then( value => ( value === control ) ? 'pending' : 'fulfilled' ) .catch( () => 'rejected' ); } const throttle = async function* ( reservoir, promiseClass, highWaterMark ) { let iterable = reservoir.splice( 0, highWaterMark ).map( item => promiseClass( item ) ); while ( iterable.length > 0 ) { await Promise.any( iterable ); const pending = []; const resolved = []; for ( const currentValue of iterable ) { if ( await promiseState( currentValue ) === 'pending' ) { pending.push( currentValue ); } else { resolved.push( currentValue ); } } console.log({ pending, resolved, reservoir }); iterable = [ ...pending, ...reservoir.splice( 0, highWaterMark - pending.length ).map( value => promiseClass( value ) ) ]; yield Promise.allSettled( resolved ); } } const getTimeout = delay => new Promise( ( resolve, reject ) => { setTimeout(resolve, delay, delay); } ); ( async () => { const test = [ 1100, 1200, 1300, 10000, 11000, 9000, 5000, 6000, 3000, 4000, 1000, 2000, 3500 ]; const throttledRequests = throttle( test, getTimeout, 4 ); for await ( const timeout of throttledRequests ) { console.log( timeout ); } } )();

控制最大承诺/请求数的一个很好的解决方案是将请求列表拆分为页面,并且一次只生成一个页面的请求。

下面的示例使用了iter-ops库:

import {pipe, toAsync, map, page} from 'iter-ops';

const i = pipe(
    toAsync(users), // make it asynchronous
    page(10), // split into pages of 10 items in each
    map(p => Promise.all(p.map(u => u.remoteServer.getCount(u)))), // map into requests
    wait() // resolve each page in the pipeline
);

// below triggers processing page-by-page:

for await(const p of i) {
    //=> p = resolved page of data
}

这样它就不会尝试创建比一页大小更多的请求/承诺。

使用tiny-async-pool ES9 for await...of API,您可以执行以下操作:

const asyncPool = require("tiny-async-pool");
const getCount = async (user) => ([user, remoteServer.getCount(user)]);
const concurrency = 2;

for await (const [user, count] of asyncPool(concurrency, users, getCount)) {
  console.log(user, count);
}

上面的 asyncPool 函数返回一个异步迭代器,一旦 Promise 完成(在并发限制下),它就会产生,并且一旦其中一个 Promise 拒绝,它就会立即拒绝。

可以使用https://www.npmjs.com/package/job-pipe限制对服务器的请求

基本上你创建一个管道并告诉它你想要多少并发请求:

const pipe = createPipe({ throughput: 6, maxQueueSize: Infinity })

然后,您使用执行调用的函数并强制它通过管道同时创建有限数量的调用:

const makeCall = async () => {...}
const limitedMakeCall = pipe(makeCall)

最后,您可以根据需要多次调用此方法,就好像它没有改变一样,它将限制自己可以处理多少并行执行:

await limitedMakeCall()
await limitedMakeCall()
await limitedMakeCall()
await limitedMakeCall()
await limitedMakeCall()
....
await limitedMakeCall()

利润。

建议不要下载包,不要写几百行代码:

async function async_arr<T1, T2>(
    arr: T1[],
    func: (x: T1) => Promise<T2> | T2, //can be sync or async
    limit = 5
) {
    let results: T2[] = [];
    let workers = [];
    let current = Math.min(arr.length, limit);
    async function process(i) {
        if (i < arr.length) {
            results[i] = await Promise.resolve(func(arr[i]));
            await process(current++);
        }
    }
    for (let i = 0; i < current; i++) {
        workers.push(process(i));
    }
    await Promise.all(workers);
    return results;
}

这是我的食谱,基于 killdash9 的回答。 它允许选择异常行为( Promise.allPromise.allSettled )。

// Given an array of async functions, runs them in parallel,
// with at most maxConcurrency simultaneous executions
// Except for that, behaves the same as Promise.all,
// unless allSettled is true, where it behaves as Promise.allSettled  

function concurrentRun(maxConcurrency = 10, funcs = [], allSettled = false) {
  if (funcs.length <= maxConcurrency) {
    const ps = funcs.map(f => f());
    return allSettled ? Promise.allSettled(ps) : Promise.all(ps);
  }
  return new Promise((resolve, reject) => {
    let idx = -1;
    const ps = new Array(funcs.length);
    function nextPromise() {
      idx += 1;
      if (idx < funcs.length) {
        (ps[idx] = funcs[idx]()).then(nextPromise).catch(allSettled ? nextPromise : reject);
      } else if (idx === funcs.length) {
        (allSettled ? Promise.allSettled(ps) : Promise.all(ps)).then(resolve).catch(reject);
      }
    }
    for (let i = 0; i < maxConcurrency; i += 1) nextPromise();
  });
}

我知道已经有很多答案,但我最终使用了一个非常简单、不需要库或睡眠的解决方案,它只使用了几个命令。 Promise.all() 只是让您知道传递给它的所有承诺何时完成。 因此,您可以间歇性地检查队列,看它是否准备好进行更多工作,如果是,则添加更多进程。

例如:

// init vars
const batchSize = 5
const calls = []
// loop through data and run processes  
for (let [index, data] of [1,2,3].entries()) {
   // pile on async processes 
   calls.push(doSomethingAsyncWithData(data))
   // every 5th concurrent call, wait for them to finish before adding more
   if (index % batchSize === 0) await Promise.all(calls)
}
// clean up for any data to process left over if smaller than batch size
const allFinishedProcs = await Promise.all(calls)

这就是我在这里的代码中使用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/

如果您的目标是减慢 Promise.all 以避免速率限制或过载:

这是我的实现

async function promiseAllGentle(arr, batchSize = 5, sleep = 50) {
  let output = [];
  while (arr.length) {
    const batchResult = await Promise.all(arr.splice(0, batchSize));
    output = [...output, ...batchResult];
    await new Promise((res) => setTimeout(res, sleep));
  }
  return output;
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM