ES6의 Promise.all ()을 사용할 때 동시성을 제한하는 가장 좋은 방법은 무엇입니까?


108

데이터베이스에서 쿼리 된 목록을 반복하고 해당 목록의 각 요소에 대해 HTTP 요청을 만드는 코드가 있습니다. 이 목록은 때때로 상당히 많은 수 (수천 개)가 될 수 있으며 수천 개의 동시 HTTP 요청이있는 웹 서버에 연결되지 않도록하고 싶습니다.

이 코드의 축약 된 버전은 현재 다음과 같습니다.

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주어진 시간에 특정 수의 약속 만 진행되도록 관리 할 수 있습니까?



3
그것이 Promise.all약속 진행을 관리한다는 것을 잊지 마십시오 . 약속은 스스로 Promise.all그것을 수행하고 단지 그들을 기다립니다.
Bergi

1
Promise생성자 반 패턴을 피하십시오 !
Bergi

답변:


53

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

26
es6-promise-pool이 Promise를 사용하는 대신 재창조 한 것은 유감입니다. 대신이 간결한 솔루션을 제안합니다 (이미 ES6 또는 ES7을 사용하는 경우) github.com/rxaviers/async-pool
Rafael Xavier

3
둘 다 살펴보면 async-pool이 훨씬 좋아 보입니다! 더 간단하고 가볍습니다.
Endless

3
또한 p-limit가 가장 간단한 구현이라는 것을 발견했습니다. 아래 내 예를 참조하십시오. stackoverflow.com/a/52262024/8177355
Matthew Rideout

2
나는 tiny-asyc-pool이 약속의 동시성을 제한하는 데 훨씬 더 좋고, 방해가되지 않으며, 오히려 자연스러운 솔루션이라고 생각합니다.
Sunny Tambi

78

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

콘솔 로그 결과는 해결 된 약속 응답 데이터의 배열입니다.


4
이것에 감사드립니다! 이것은 훨씬 더 간단합니다
John

3
이것은 동시 요청을 제한하는 데있어 지금까지 본 최고의 라이브러리였습니다. 그리고 좋은 예, 감사합니다!
Chris Livdahl

3
비교해 주셔서 감사합니다. github.com/rxaviers/async-pool 과 비교해 보셨습니까 ?
ahong

1
사용하기 쉽고 훌륭한 선택입니다.
drmrbrewer

1
이 외에도 매주 npm에서 약 2 천만 회 다운로드가 있으며 다른 답변에서 언급 한 다른 libs는 약 200-100k가 있습니다.
vir us

25

사용 Array.prototype.splice

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

3
이것은 저평가 된 솔루션입니다. 단순함을 좋아하십시오.
Brannon

12
이렇게하면 풀 대신 일괄 적으로 함수가 실행됩니다. 여기서 한 함수는 다른 함수가 완료되면 즉시 호출됩니다.
cltsang

이 솔루션을 좋아했습니다!
prasun

예를 들어 풀 대신 배치를 사용하는 것과 같이 더 많은 컨텍스트가 부족하여 무엇을하는지 파악하는 데 1 초가 걸렸습니다. 처음부터 또는 중간에 접속할 때마다 어레이의 순서를 변경합니다. (브라우저는 모든 항목을 다시 색인화해야 함) 이론적 성능이 더 나은 대안은 arr.splice(-100)순서가 더 이상 맞지 않으면 끝에서 물건을 가져 오는 것 입니다. 아마도 배열을 뒤집을 수 있습니다. : P
Endless

2
일괄 실행에 매우 유용합니다. 참고 : 다음 배치는 현재 배치가 100 % 완료 될 때까지 시작되지 않습니다.
Casey Dwayne

24

반복기가 작동하는 방식과 사용 방식을 알고 있다면 별도의 라이브러리가 필요하지 않을 것입니다. 자신 만의 동시성을 구축하는 것이 매우 쉬워 질 수 있기 때문입니다. 시연하겠습니다.

/* [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]]은 2의 동시성으로 아래에서 설명합니다.

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 jul.

이것은 확실히 멋진 접근 방식입니다! 추가로 끝날 수 있으므로 동시성이 작업 목록의 길이를 초과하지 않는지 확인하십시오 (어쨌든 결과에 관심이 있다면).
Kris Oye

나중에 더 멋진 것은 Streams가 Readable.from (iterator) 지원을 받을 때 입니다. Chrome은 이미 스트림을 전송할 수 있도록했습니다 . 따라서 읽을 수있는 스트림을 만들어 웹 워커에게 보낼 수 있으며 모두 동일한 기본 반복자를 사용하게됩니다.
Endless

16

bluebird의 Promise.map 은 동시성 옵션을 사용하여 병렬로 실행해야하는 promise의 수를 제어 할 수 있습니다. 때로는 .allpromise 배열을 만들 필요 가 없기 때문에 더 쉽습니다 .

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
}

블루 버드는 더 빠른 약속이 필요한 경우 화격자이며 한 가지 용도로만 사용하는 경우 ~ 18kb 추가 정크;)
Endless

2
모든 것은 한 가지가 당신에게 얼마나 중요한지 그리고 다른 빠르고 쉬운 더 나은 방법이 있는지에 달려 있습니다. 전형적인 트레이드 오프. 몇 kb 이상의 사용 편의성과 기능을 선택하지만 YMMV입니다.
Jingshao Chen

13

http 요청을 제한하기 위해 promise를 사용하는 대신 노드의 기본 제공 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}, ...);

동일한 오리진에 여러 요청을하는 경우 keepAlivetrue 로 설정 하는 것도 도움이 될 수 있습니다 (자세한 내용은 위의 문서 참조).


12
그래도 수천 개의 클로저를 즉시 생성하고 소켓을 풀링하는 것이 매우 효율적이지 않은 것 같습니까?
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
나를 위해 작동합니다. 감사. 이것은 훌륭한 도서관입니다.
Sunny Tambi

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

복사-붙여 넣기에 친숙하고 동시성 제한이있는 완전한 Promise.all()/ map()대체 기능에 대한 ES7 솔루션 입니다.

이와 유사하게 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/ async-pool / 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 패키지 배치 약속을 사용하는 것이 가장 쉬운 경로였습니다.

참고 : 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
Endless


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

이 접근 방식은 scala / java의 스레드 풀과 유사한 멋진 API를 제공합니다.
를 사용하여 풀의 한 인스턴스를 만든 후 const cappedPool = new Pool(2)간단히 cappedPool.add(() => myPromise).
당연히 우리는 약속이 즉시 시작되지 않도록해야하며, 이것이 바로 기능의 도움으로 "게으르게 제공"해야하는 이유입니다.

가장 중요한 것은 방법의 결과가 add 원래 약속의 가치로 완료 / 해결 될 약속이라는 것입니다 ! 이것은 매우 직관적 인 사용을 가능하게합니다.

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

0

안타깝게도 네이티브 Promise.all로는 할 수있는 방법이 없으므로 창의력을 발휘해야합니다.

이것은 외부 라이브러리를 사용하지 않고 찾을 수있는 가장 간결한 방법입니다.

반복 자라고하는 최신 자바 스크립트 기능을 사용합니다. 반복기는 기본적으로 처리 된 항목과 처리되지 않은 항목을 추적합니다.

코드에서 사용하기 위해 비동기 함수 배열을 만듭니다. 각 비동기 함수는 처리해야하는 다음 항목에 대해 동일한 반복기를 요청합니다. 각 함수는 자체 항목을 비동기 적으로 처리하고 완료되면 반복기에 새 항목을 요청합니다. 반복기에 항목이 부족하면 모든 기능이 완료됩니다.

영감을 주신 @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);


0

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

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.