async/awaitとPromiseでjavascript直列/並列処理

author potpro(ぼとぷろ)
2016/12/09

async/awaitとPromiseでjavascript直列/並列処理

potproject.net Advent Calendar 201610日目の記事です。
祝!10日連続更新!


ES8にあたるECMAScript2017では、await/asyncが実装されると聞いて。黙っちゃいられねえ。
まだES2017は策定中ですが、なんかEdgeでも動くようになったらしいし(設定は必要)、多分ほぼ確定。内定状態でしょう。
https://tc39.github.io/ecma262/
しかし、やはり標準では動かないので、このコードはbabelでトランスパイルしてnode.jsで動かしてます。ご了承ください。

await/async修飾子は、元々C#で使われていた非同期処理を同期的に記述できる仕組みです。
一時期C#を使っていてユニバーサル Windows プラットフォーム (UWP) アプリを作っていた時があり、
制約がありすぎて普通にWindows formsで作りてえと思いつつその時に使用しておりました。

基本的に使い方はC#もjavascriptも同じです。しかし、当然javascriptにはTask型なんてありませんので、Promiseがそれの代用になります。
つまりは、await/asyncを使うにはPromiseから習得する必要があります。
なので、ここではPromiseに関しても軽く記載しておきます。

Promiseに関しては、非同期の処理を上手く同期的に見せたり、並列処理が出来るメソッドです。
何が違うかというと、書き方が違います。await/asyncは非同期処理を同期的に行うため、結局直列の処理になりますが、Promiseは並列処理が可能です。
今回書くコードは、Promise.allでの並列処理とawait/asyncでの直列処理の比較です。
プログラム的には、1から指定された数まで全て足して出力するだけの処理です。
arr.reduceを使った非同期処理方法とfor文を使った同期処理方法で、それぞれ処理をして表示します。
一応速度も出力します。

//繰り返し回数
const execute_count=10000000;
(async ()=>{
  //await/asyncでの直列処理
  var startTime = new Date();
  var sumtest = await reduce_sum(execute_count);
  var normalsumtest=normal_sum(execute_count);
  var endTime = new Date();
  if(sumtest===normalsumtest){
    console.log("(await/async):" + (endTime - startTime) + "ms ans:"+sumtest);
  }

  //Promise.allでの並列処理
  var startTime2 = new Date();
  var sumexecute=await sum(execute_count);
  var endTime2 = new Date();
  console.log("(Promise.all):"+(endTime2 - startTime2)+ "ms ans:"+sumexecute);
})();

async function sum(num){
  return new Promise((resolve,reject)=>{
    Promise.all([normal_sum(num), reduce_sum(num)])
    .then(function(sum_result) { 
      if(sum_result[0]===sum_result[1]){
        resolve(sum_result[0]);
      }else{
        reject();
      }
    });
    
  });
  
}

function normal_sum(num){
  var arr=[];
  for(var i=1;i<=num;i++){
      arr.push(i);
  }
  var sum = 0;
  for(var x=0;x<arr.length;x++){
    sum+=arr[x];
  }
  return sum;
  
}

async function reduce_sum(sum){
    return new Promise((resolve,reject)=>{
      var arr=[];
      for(var i=1;i<=sum;i++){
        arr.push(i);
      }
      return resolve(arr.reduce(function(prev, current) {
        return prev+current;
      }));
    });
}

結果はこんな感じ。

** (await/async):690ms ans:50000005000000 **
** (Promise.all):786ms ans:50000005000000 **

並列の方が遅い。ほぼ変わらずです。なんで?って感じですが、多分これが正しいと思います。
javascriptはご存知の通りシングルスレッドなので、並列処理だからってスレッドを増やしたりできません。
シングルスレッドということは同時に別の処理をやることはできないので、だから結局のところ疑似並列処理なだけです。
むしろ並列にしたほうが割り込みが入るので、遅くなると思います。
実際トランスパイラしてるというのもあるし、await/async/Promiseの処理時間とかもあるので上の時間は多分あてになりません。

一番の問題は、コードの見やすさです。
await/asyncを使えばメインのコードにコールバックを一切入れることが無くなるため、見やすいんじゃないかなと思います。
自分は積極的にawait/asyncは使っていきたいです。もうコールバック地獄には入りたくない・・・。