2017年11月11日土曜日

JavaScriptにおけるスレッドをブロックしないループ処理の実現

JavaScriptは通常シングルスレッドで動作しており, もともと「すぐに終わる処理」の実行のみが想定・推奨されている. が, 昨今のWEB環境の発展に伴い, 以前とは比較にならない程複雑で負荷の高い処理がブラウザ上で行われるようになっている.

これらの問題の幾つかはWeb Workerを用いたマルチスレッドの導入により解決するが, DOMやフォントを扱う必要がある場合などはどうしてもメインスレッド上で処理をせざるを得ない.

そこでES2016/2017で導入されたJavaScript言語機構を使って, 負荷の高い処理を複数のタスクに分割しそれらを非同期的に順次実行することでブラウザの応答性を改善する方法を考えてみた.


以下はそのサンプルである.


"use strict";
//渡されたgenerator関数を非同期で繰り返し実行する
//実行結果をPromiseとして返すので, ループ処理の完了を待つことが出来る
const nonBlockingLoop = (generator, dur = 500) => new Promise((resolve, reject) => {
 let current = Date.now();
 const itr = generator();//generatorからIteratorを生成する
 (function f() {
  try {
   while (!itr.next().done) {
    const now = Date.now();
    if (now - current > dur) {
     return current = now, setTimeout(f, 0);//時間上限なので続きを非同期処理としてスケジュールする
    }
   }
   resolve();
  } catch (e) {
   reject(e);
  }
 })();
});
//使用例
(async function () {
 const canvas = document.querySelector("canvas");
 canvas.width = 1024, canvas.height = 1024;
 const ctx = canvas.getContext("2d");
 try {
  //function *でgeneratorを定義し, その中にループ処理を記述する
  //await宣言で処理の終了を待つ
  await nonBlockingLoop(function* () {
   for (let j = 0, h = canvas.height; j < h; j++) {
    for (let i = 0, w = canvas.width; i < w; i++) {
     ctx.fillStyle = `hsl(${360 * i / w}, 100%, ${j / h * 100}%)`;
     ctx.fillRect(i, j, 1, 1);
     yield;//処理を中断しうる箇所にyieldを記述する
    }
   }
  }, 100);
  console.log("completed.");
 } catch (e) {
  console.log(e);
 }
})();

実際の動作例はこちら. 巨大なcanvas要素に対する描画がブラウザの動作を損なうことなく徐々に行われることが判る.

また通常のループ処理に複数行専用の記述が追加されているだけでコードの見通しが非常に良い.

用いた言語機構は次の通り.
  1. ループ処理の中断・継続にはfunction*によるジェネレータ関数を用いる.
    ジェネレータ関数はyieldステートメントで処理を中断出来る. ジェネレータ関数はイテレータを生成(つまりループ処理のイテレータ化)する.

  2. イテレータをループで処理する際, 指定時間経ったらsetTimeoutを使って次の処理呼び出しを非同期で行う
     
  3. ループ処理の完了を通知するため, Promiseを用いる.
     
  4. Promiseによる処理完了の通知はasync関数とawaitステートメントを用いることで”同期的なコード”で記述できる.

0 件のコメント:

コメントを投稿