[英]What are the differences between jQuery.when() and ES6's 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);
});
只要有可能,我都会尝试自己开发这种东西,而不是去图书馆。 你最终会学习很多以前看起来令人生畏的概念。
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 仅在需要时实例化,以减少内存消耗。 可以使用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.all
与Promise.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.