awaitの取り入れ方

awaitの取り入れ方:

これまでの記事で、以下のことを示しました。


  • awaitを使う理由は、別の処理に実行権を譲ること

  • awaitを使うと、プログラムの複雑さが増す
  • まずはawaitを使わずに実装し、時間のかかる処理からawaitを取り入れる
今回の記事では、どのようにしてawaitを取り入れるかについて説明していきます。


awaitの取り入れ方

例として、以下のコードにawaitを取り入れることを考えます。

const heavy = () => { 
  for (let i = 0; i < 1000; i++) { 
    light();  // 0.01秒 
  } 
}; 
light()には0.01秒かかり、それを1000回呼ぶheavy()は10秒(0.01秒×1000)かかります。この処理にはawaitがないため、heavy()を呼ぶと10秒間フリーズします。ここにawaitを取り入れます。

取り入れる手順は以下になります。


  1. sleep()を定義する

  2. await sleep(0);を適切な場所に記述する
  3. 関数の定義にasyncを付ける
sleep()の定義にはいくつかバリエーションがありますが、今回はwindow.setTimeout()を使った実装にします。次にawait sleep(0);の記述場所ですが、具体的には「軽い処理と軽い処理の間」が記述に適した場所です。ということで、light();の前もしくは後に記述します。

awaitを取り入れたコードは次のようになります。

const sleep = msec => { 
  return new Promise(resolve => { 
    window.setTimeout(() => resolve(), msec); 
  }); 
}; 
 
const heavy = async () => { 
  for (let i = 0; i < 1000; i++) { 
    light(); 
    await sleep(0); 
  } 
}; 
このコードでは、light()呼び出し後に別の処理に実行権を譲っています。したがって、フリーズを体感することなくheavy()の処理が終わります。


実行権を譲る=遅い

実行権を譲るということは、一度実行権を手放すということです。一度手放した実行権が戻ってくるのには時間がかかるため、手放す回数が多いほど、await sleep(0);にかかる時間の割合は大きくなります。具体的には、await sleep(0);行をコメントアウトするかどうかで実行時間が4秒以上変わります。

実際に動かしたコード

const sleep = msec => { 
  return new Promise(resolve => { 
    window.setTimeout(() => resolve(), msec); 
  }); 
}; 
 
const light = () => { 
  const start = new Date().getTime(); 
  while (new Date().getTime() - start < 10); 
}; 
 
const heavy = async () => { 
  for (let i = 0; i < 1000; i++) { 
    light(); 
    await sleep(0);  // ここをコメントアウト 
  } 
}; 
 
(async () => { 
  const s = performance.now(); 
  await heavy(); 
  console.log(performance.now() - s); 
})(); 
このように、awaitの回数に応じて実行時間は長くなるため、極力awaitの回数を少なくしたいです。そこでheavy()を次のように書き換えます。

const heavy = async () => { 
  let c = 0; 
  for (let i = 0; i < 1000; i++) { 
    light(); 
    c++; 
    if (c % 3 === 0) { 
      c = 0; 
      await sleep(0); 
    } 
  } 
}; 
書き換え前のコードではawaitが1000回実行されていましたが、書き換え後のコードでは333回になりました。これは、light()を3回実行してからawaitを1回実行するということです。間に0.03秒のフリーズが発生しているため、少しカクつくかな?と感じるかもしれませんが、その代わりに実行時間が2.6秒以上短縮されます。

このあたりは、実際に動かしながらチューニングする部分です。3回に1回ではカクつきがひどいと感じるかもしれません。そういったときは、c % 3 === 032に書き換えます。逆に、カクつきがひどくてもいいから早く処理を終わらせたい場合は345に書き換えます。このように、実際に動かしながら妥協点を探していきます。


heavy()の変貌

heavy()の修正前と修正後のコードを比較します。

const heavy = () => { 
  for (let i = 0; i < 1000; i++) { 
    light(); 
  } 
}; 
const heavy = async () => { 
  let c = 0; 
  for (let i = 0; i < 1000; i++) { 
    light(); 
    c++; 
    if (c % 3 === 0) { 
      c = 0; 
      await sleep(0); 
    } 
  } 
}; 
修正前のコードは読みやすいです。heavy()light()を1000回呼び出す関数だということがひと目で分かります。しかし、修正後のコードはごちゃっとしており、ひと目ではheavy()が何をしているかがわかりません。

なぜごちゃっとしているかというと、「light()を1000回呼び出す処理」と「3回に1回スリープする処理」が一つのブロック内に記述されているからです。


yieldを使って処理を分離

この2つの処理を分離する方法として、yieldを使う方法があります。ということで、実際にheavy()yieldを取り入れてみます。

まず、heavy()の適切な場所にyieldを記述します。「適切な場所」というのは、await sleep(0);のときと同じで「軽い処理と軽い処理の間」です。

const heavy = function* () { 
  for (let i = 0; i < 1000; i++) { 
    light(); 
    yield; 
  } 
}; 
次に、heavy()の呼び出し側のコードを次のように書き換えます。

(async () => { 
  for (var _ of heavy()) { 
    await sleep(0); 
  } 
})(); 
これでyieldによる修正は終わりです。

この修正の優れているところは、「light()を1000回呼び出す処理」と「スリープする処理」が完全に分かれていることです。

スリープする回数を3回に1回の頻度に減らしてみます。このとき、heavy()を一切書き換えることなく修正が完了します。

const heavy = function* () { 
  for (let i = 0; i < 1000; i++) { 
    light(); 
    yield; 
  } 
}; 
 
(async () => { 
  let c = 0; 
  for (var _ of heavy()) { 
    c++; 
    if (c % 3 === 0) { 
      c = 0; 
      await sleep(0); 
    } 
  } 
})(); 
まったくスリープしないという選択もできます。もちろん、heavy()を修正する必要はありません。

(() => { 
  for (var _ of heavy()); 
})(); 
yieldawaitを使うことで、2つの異なる処理を綺麗に分離できることがわかりました。


まとめ

この記事では、awaitの取り入れ方について解説しました。そして、yieldawaitを組み合わせることで、2つの異なる処理を綺麗に分離できることを示しました。

コメント

このブログの人気の投稿

投稿時間:2021-06-20 02:06:12 RSSフィード2021-06-20 02:00 分まとめ(3871件)

投稿時間:2021-04-30 23:37:32 RSSフィード2021-04-30 23:00 分まとめ(42件)

投稿時間:2023-02-05 02:09:04 RSSフィード2023-02-05 02:00 分まとめ(9件)