Node.jsのイベント駆動型プログラミングモデルでは、プログラムフローを調整するのが多少難しくなります。
単純な順次実行はネストされたコールバックに変換されますが、これは十分に簡単です(書き留めるのは少し複雑です)。
しかし、並列実行はどうでしょうか?並行して実行できる3つのタスクA、B、Cがあり、それらが完了したらタスクDに結果を送信するとします。
フォーク/結合モデルでは、これは
Node.jsでどのように書くのですか?ベストプラクティスや料理の本はありますか? ソリューションを手動でロールする 毎回する必要がありますか、またはこれを支援するライブラリがありますか?
Node.jsはシングルスレッドであるため、真に並列的なものはありません。ただし、複数のイベントをスケジュールして、事前に決定できない順序で実行することができます。また、データベースアクセスのようなものは、データベースクエリ自体が個別のスレッドで実行されますが、完了時にイベントストリームに再統合されるという点で、実際には「並列」です。
それでは、複数のイベントハンドラーでコールバックをどのようにスケジュールしますか?さて、これはブラウザ側JavaScriptのアニメーションで使用される一般的なテクニックの1つです。完了を追跡するために変数を使用します。
これはハックのように聞こえますが、それは厄介であり、追跡の実行とそれよりも少ない言語でのグローバル変数の束を残す可能性があります。しかし、javascriptではクロージャーを使用できます。
_function fork (async_calls, shared_callback) {
var counter = async_calls.length;
var callback = function () {
counter --;
if (counter == 0) {
shared_callback()
}
}
for (var i=0;i<async_calls.length;i++) {
async_calls[i](callback);
}
}
// usage:
fork([A,B,C],D);
_
上記の例では、非同期関数とコールバック関数が引数を必要としないことを想定して、コードをシンプルにしています。もちろん、非同期関数に引数を渡すようにコードを変更し、コールバック関数に結果を蓄積させ、それをshared_callback関数に渡すことができます。
実際、そのままでも、fork()
関数は既にクロージャーを使用して非同期関数に引数を渡すことができます:
_fork([
function(callback){ A(1,2,callback) },
function(callback){ B(1,callback) },
function(callback){ C(1,2,callback) }
],D);
_
あとは、A、B、Cからの結果を蓄積してDに渡すだけです。
抵抗できませんでした。朝食中にこれについて考え続けた。以下は、結果を蓄積するfork()
の実装です(通常、コールバック関数に引数として渡されます)。
_function fork (async_calls, shared_callback) {
var counter = async_calls.length;
var all_results = [];
function makeCallback (index) {
return function () {
counter --;
var results = [];
// we use the arguments object here because some callbacks
// in Node pass in multiple arguments as result.
for (var i=0;i<arguments.length;i++) {
results.Push(arguments[i]);
}
all_results[index] = results;
if (counter == 0) {
shared_callback(all_results);
}
}
}
for (var i=0;i<async_calls.length;i++) {
async_calls[i](makeCallback(i));
}
}
_
それは十分簡単でした。これにより、fork()
がかなり汎用的になり、複数の不均一なイベントを同期するために使用できます。
Node.jsでの使用例:
_// Read 3 files in parallel and process them together:
function A (c){ fs.readFile('file1',c) };
function B (c){ fs.readFile('file2',c) };
function C (c){ fs.readFile('file3',c) };
function D (result) {
file1data = result[0][1];
file2data = result[1][1];
file3data = result[2][1];
// process the files together here
}
fork([A,B,C],D);
_
このコードは、async.jsなどのライブラリまたはさまざまなpromiseベースのライブラリが存在する前に作成されました。私はasync.jsがこれに触発されたと信じたいのですが、その証拠はありません。とにかく..今日これを行うことを考えているなら、async.jsまたは約束を見てください。上記の答えを、async.parallelのようなものがどのように機能するかについての良い説明/図として考えてください。
完全を期すために、以下に_async.parallel
_を使用した方法を示します。
_var async = require('async');
async.parallel([A,B,C],D);
_
_async.parallel
_は、上で実装したfork
関数とまったく同じように機能することに注意してください。主な違いは、node.jsの規則に従って、D
の最初の引数としてエラーを渡し、2番目の引数としてコールバックを渡すことです。
約束を使用して、次のように記述します。
_// Assuming A, B & C return a promise instead of accepting a callback
Promise.all([A,B,C]).then(D);
_
現在、「非同期」モジュールはこの並列機能を提供し、上記のfork関数とほぼ同じであると考えています。
futures モジュールには join というサブモジュールがあり、これを使用するのが好きです:
pthread_join
がスレッドに対して機能する方法と同様に、非同期呼び出しを結合します。
Readmeは、フリースタイルの使用またはPromiseパターンを使用した future サブモジュールの使用の良い例を示しています。ドキュメントの例:
var Join = require('join')
, join = Join()
, callbackA = join.add()
, callbackB = join.add()
, callbackC = join.add();
function abcComplete(aArgs, bArgs, cArgs) {
console.log(aArgs[1] + bArgs[1] + cArgs[1]);
}
setTimeout(function () {
callbackA(null, 'Hello');
}, 300);
setTimeout(function () {
callbackB(null, 'World');
}, 500);
setTimeout(function () {
callbackC(null, '!');
}, 400);
// this must be called after all
join.when(abcComplete);
別のオプションは、ノードのステップモジュールです。 https://github.com/creationix/step
ここでは簡単な解決策が可能かもしれません: http://howtonode.org/control-flow-part-ii パラレルアクションまでスクロールします。別の方法は、A、B、およびCがすべて同じコールバック関数を共有し、その関数にグローバルまたは少なくとも機能外のインクリメンターを持たせることです。3つすべてがコールバックを呼び出してからDを実行します。もちろん、A、B、Cの結果もどこかに保存する必要があります。
一般的な約束と非同期ライブラリに加えて、3番目のエレガントな方法があります-「配線」を使用します:
var l = new Wire();
funcA(l.branch('post'));
funcB(l.branch('comments'));
funcC(l.branch('links'));
l.success(function(results) {
// result will be object with results:
// { post: ..., comments: ..., links: ...}
});