web-dev-qa-db-ja.com

ES6の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のみが常に進行中であるようにできますか?

53
Chris

Promise.all()は、約束を作成してトリガーを作成するのではなく、約束を作成することに注意してください。

それを念頭に置いて、1つの解決策は、新しいプロミスを開始する必要があるか、すでに限界に達しているかどうかをプロミスが解決されるたびにチェックすることです。

ただし、ここで車輪を再発明する必要はありません。 この目的に使用できるライブラリの1つは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)
})
37
Timo

P-Limit

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

コンソールログの結果は、解決済みのプロミスレスポンスデータの配列です。

27
Matthew Rideout

bluebirdの Promise.map は、並行オプションを使用して、並行して実行するプロミスの数を制御できます。 promise配列を作成する必要がないため、.allより簡単な場合があります。

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
}
11
Jingshao Chen

HTTPリクエストを制限するためにプロミスを使用する代わりに、ノードの組み込み http.Agent.maxSockets を使用します。これにより、ライブラリを使用したり、独自のプーリングコードを記述したりする必要がなくなり、制限対象をより詳細に制御できるという利点があります。

agent.maxSockets

デフォルトでは、Infinityに設定されています。オリジンごとにエージェントがオープンできる同時ソケットの数を決定します。 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}, ...);

同じOriginに対して複数のリクエストを行う場合、keepAliveをtrueに設定することも有益です(詳細については上記のドキュメントを参照してください)。

5
tcooc

イテレータがどのように機能し、どのように消費されるかを知っていれば、余分なライブラリは必要ありません。自分で並行処理を簡単に構築できるからです。デモさせてください:

/* [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]で2D配列を取得したことになります。

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

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

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

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

注:例との違い async-pool は、2人のワーカーを生成するため、1人のワーカーがインデックス5で何らかの理由でエラーが発生しても、他のワーカーが他の作業を停止することはありません。したがって、2つの同時実行を1に減らします(そこで停止しません)。そして、すべてのワーカーがいつ終了するかを知るのは、1つが失敗するとPromise.allが早く救済されるため、難しくなります。したがって、doWork関数内のすべてのエラーをキャッチすることをお勧めします

5
Endless

再帰を使用して解決できます。

最初は、許可された最大数のリクエストを送信し、これらの各リクエストは完了時に再帰的に自身を送信し続ける必要があるという考えです。

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);
});
1
Anton Fil

ストリーミングと「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
            );
        }
1
gosuer1921

外部ライブラリを使用したくない場合は、再帰が答えです

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
Juan

これは、私のコード内で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/

0
Marcelo Rafael

だから私は自分のコードでいくつかの例を動作させようとしましたが、これは本番コードではなくインポートスクリプトのためだけだったので、npmパッケージを使用してください batch-promises は確かに私にとって最も簡単なパスでした

注:Promiseをサポートするため、またはポリフィルするためにランタイムが必要です。

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