2013-04-12 20 views
10

私はずっとJavaScriptで開発してきましたが、ネットではまだカウボーイの開発者です。いつも私を悩ませている多くの事の一つは、JavaScriptのコールバックを同期させることです。JavaScriptコールバックの同期方法

この問題が発生する一般的なシナリオについて説明します。forループで複数回実行する操作があり、各操作にコールバックがあります。 forループの後で、別の操作を実行する必要がありますが、この操作はforループからのすべてのコールバックが完了した場合にのみ正常に実行できます。

コード例:

for ... in ... { 
    myFunc1(callback); // callbacks are executed asynchly 
} 

myFunc2(); // can only execute properly if all the myFunc1 callbacks are done 

推奨解決策:

は、ループの長さを保持しているループの開始時にカウンタを開始し、そして各コールバックはそれをデクリメントカウンタ。カウンタが0に達すると、myFunc2を実行します。これは本質的に、コールバックが最後のコールバックであるかどうかを順番に知らせ、そうであれば、完了したらmyFunc2を呼び出します。

問題:

  1. カウンタは、あなたのコード内のすべてのそのような配列のために必要な、そしてどこでも無意味なカウンターを持つことは良い習慣ではありません。
  2. あなたは、古典的な同期の問題でどのようにスレッドの競合をリコールした場合、複数のスレッドがすべて同じVARにvar--呼びかけているとき、望ましくない結果が発生します。 JavaScriptでも同じことが起こりますか?

究極の質問:

は、よりよい解決策はありますか?それらの一つ一つが、別の関数であり、変数は(varで)正しく宣言されているよう

+0

パラレルにまたは次々に起こるはずですか? –

+0

彼はそれを次々と望むなら、彼は非同期タスクを使用しないでしょう。 – jelgh

+0

@jelgh非同期タスクをシリアライズする必要があるとは限りません。 –

答えて

11

良いニュースは、JavaScriptがシングルスレッドであることです。これは、ソリューションが一般に「共有」変数でうまくいくことを意味します。つまり、ミューテックスロックは必要ありません。

あなたはこのヘルパー関数を使用することができ、完了コールバックが続く非同期タスク、シリアライズしたい場合:

function serializeTasks(arr, fn, done) 
{ 
    var current = 0; 

    fn(function iterate() { 
     if (++current < arr.length) { 
      fn(iterate, arr[current]); 
     } else { 
      done(); 
     } 
    }, arr[current]); 
} 

を最初の引数が二、各パスに渡す必要がある値の配列です引数はループコールバック(後で説明する)で、最後の引数は補完コールバック関数です。

これは、ループのコールバック関数です:

function loopFn(nextTask, value) { 
    myFunc1(value, nextTask); 
} 

渡された最初の引数は次のタスクを実行する関数である、あなたの非同期関数に渡されることを意味しています。 2番目の引数は、値の配列の現在のエントリです。

のは、非同期タスクは次のようになりますと仮定しましょう:

function myFunc1(value, callback) 
{ 
    console.log(value); 
    callback(); 
} 

それは値を出力し、その後、それはコールバックが呼び出されます。シンプル。

function parallelizeTasks(arr, fn, done) 
{ 
    var total = arr.length, 
    doneTask = function() { 
     if (--total === 0) { 
     done(); 
     } 
    }; 

    arr.forEach(function(value) { 
     fn(doneTask, value); 
    }); 
} 

そして、あなたのループ機能は、これを次のようになります。

serializeTasks([1,2, 3], loopFn, function() { 
    console.log('done'); 
}); 

Demo

はあなたが別の機能を必要とする、それらを並列化:

そして、動きで全体のことを設定します(パラメータ名のみ変更):

function loopFn(doneTask, value) { 
    myFunc1(value, doneTask); 
} 

Demo

+0

Thx @Jack、後で詳しく説明しますが、今すぐ眠らなければなりません:) –

+0

@Xavier_Exあまりにもパラレルバージョンを追加しました:) –

+0

おかげさまでありがとうございました。すべての投稿は素晴らしいですが、この投稿のみが私の2番目の質問に答えました。提案されたソリューションのほとんどは、ある種のカウンタの使用を伴いましたが、約束は有望です。私はもう少しそれを探ると思う:) –

2

は、第二の問題は、限り、実際には問題はありません。関数内のローカル変数は相互に干渉しません。

最初の問題は、問題のもう少しです。他の人たちも迷惑をかけるようになり、図書館をあなたのためにそのようなパターンを包むようにしてしまいました。私はasyncが好きです。これにより、コードは次のようになります。

​​

その他の非同期ビルディングブロックも提供されています。私はあなたが非同期のものをたくさんやっている場合は、それをご覧になることをお勧め。

+0

;関数は常に2つのパラメータを取ると仮定していますか?2番目のパラメータはコールバックですか? –

+0

共有していただきありがとうございますが、私は2番目の問題はすべての関数が同じ共有変数(私の場合はカウンター)を減らしているが、自分のローカル変数を減らしているので問題になると思います。 –

+0

@ジャック:はい、そうだと思います。 – icktoofay

2

あなたはjQueryの繰延オブジェクトを使用することによって、これを達成することができます。このヘルパー関数で

var deferred = $.Deferred(); 
var success = function() { 
    // resolve the deferred with your object as the data 
    deferred.resolve({ 
     result:...; 
    }); 
}; 
+0

共有してくれてありがとう、私はjquery 'deferred'が最初にどのように動作するかを見てみる必要があるかもしれません。 –

+0

これは、約束を使うのと同じ解決策だと思います。その後、約束を読み上げる必要があります。 –

1

function afterAll(callback,what) { 
    what.counter = (what.counter || 0) + 1; 
    return function() { 
    callback(); 
    if(--what.counter == 0) 
     what(); 
    }; 
} 

あなたのループは次のようになります。

function whenAllDone() { ... } 
for (... in ...) { 
    myFunc1(afterAll(callback,whenAllDone)); 
} 

ここafterAllは、それはまた、カウンタをデクリメントし、コールバックのプロキシ機能を作成します。すべてのコールバックが完了したら、whenAllDone関数を呼び出します。

+0

共有いただきありがとうございますが、これは最終的にはカウンタと同じ方法です。私は一般大衆について知らないが、無意味なカウンターが迷惑なものだと気付く。 –

+0

@Xavier_Exあなたのコメントは分かりません。タスクの解決方法は何らかのカウンターを使用します。 'afterAll'をライブラリ関数とみなし、必要な場所に配置してください。 –

+0

はい、カウンターを使用するのが唯一の方法だと私が知りたいことです。私はいくつかの並行性の問題がコルーチンによって解決できることを知っているので、ここで可能性を探求したい。最終的には、カウンタが私の好みの解決策ではなく、唯一の方法であるかどうかは分かりません。 –

1

シングルスレッドは必ずしも保証されません。間違ってはいけません。

ケース1: たとえば、次の2つの関数があるとします。

var count=0; 
function1(){ 
    alert("this thread will be suspended, count:"+count); 
} 
function2(){ 
    //anything 
    count++; 
    dump(count+"\n"); 
} 

その後はfunction1が戻る前に、関数2は、1つのスレッドが保証されている場合は、関数2は、関数1が戻る前に呼び出されることはありません、と呼ばれます。これを試すことができます。あなたは警告されている間カウントが上昇していることが分かります。

ケース2:Firefox、クロムコードでは、1つの関数が戻る前に(アラートが内部にない)、別の関数を呼び出すこともできます。

実際には、mutexロックが必要です。

0

これを達成する方法はたくさんありますが、これらの提案が役立つことを願っています!

まず、私はコールバックを約束に変えるでしょう!

function aPromise(arg) { 
    return new Promise((resolve, reject) => { 
     aCallback(arg, (err, result) => { 
      if(err) reject(err); 
      else resolve(result); 
     }); 
    }) 
} 

次に、配列の要素を1つずつ処理するには、reduceを使用してください。

const arrayOfArg = ["one", "two", "three"]; 
const promise = arrayOfArg.reduce(
    (promise, arg) => promise.then(() => aPromise(arg)), // after the previous promise, return the result of the aPromise function as the next promise 
    Promise.resolve(null) // initial resolved promise 
    ); 
promise.then(() => { 
    // carry on 
}); 

アレイのすべての要素を同時に処理する場合は、Promise.all!マップを使用してください。あなたは非同期を使用することができます場合は

const arrayOfArg = ["one", "two", "three"]; 
const promise = Promise.all(arrayOfArg.map(
    arg => aPromise(arg) 
)); 
promise.then(() => { 
    // carry on 
}); 

は/あなたは、単にこれを行うことができ、次に待つ:

const arrayOfArg = ["one", "two", "three"]; 
for(let arg of arrayOfArg) { 
    await aPromise(arg); // wow 
} 

// carry on 

あなたも、このように私の非常にクールなsynchronize-asyncライブラリを使用する場合があります。

const arrayOfArg = ["one", "two", "three"]; 
const context = {}; // can be any kind of object, this is the threadish context 

for(let arg of arrayOfArg) { 
    synchronizeCall(aPromise, arg); // synchronize the calls in the given context 
} 

join(context).then(() => { // join will resolve when all calls in the context are finshed 
    // carry on 
}); 

最後に、約束を使用したくない場合は、asyncという細かいライブラリを使用してください。

const arrayOfArg = ["one", "two", "three"]; 
async.each(arrayOfArg, aCallback, err => { 
    if(err) throw err; // handle the error! 
    // carry on 
}); 
関連する問題