MENU

JavaScript同期・非同期ArrayBuffer

【JavaScript】 ブラウザでの並列処理はとても簡単Web Workerの使い方

更新日:2021/11/02

仕様書(ECMAScript)上では、JavaScriptは同期/非同期に関係なく、一つのスレッドで動作することになっていて、並列処理を行う機能は定義されていません。

 

しかし現行のブラウザの多くは、Web WorkerというAPIが組み込まれていて、これを利用することで並行処理を実現することができます。

 

この記事は、実際使ってみたら、とても簡単に並列処理できたよというお話です。

 

 

Web Worker APIについて

 

Web Worker APIと、並列処理に必要なスレッドについて簡単に説明してみます。
必要ないという方は、並列処理の基本コードに飛んでください。

 

処理はスレッド単位でおこなう

 

プログラミングでのスレッドは、プログラムコードの処理の単位をあらわします。

 

コンピューター上でプログラムを動作させると、コードが順番に読み取られて実行されていきます。
このとき、一つのスレッドで動作しています。

 

同時に別のコードを実行したい場合は、別のスレッドを用意する必要があります。

 

スレッドはプログラムを実行する単位

 

なお、一つのアプリケーションで一つのスレッドを使用するという意味ではありません。
一つのアプリケーションで複数のスレッドを作成して実行することもできます。

 

タブ一つでスレッド一つ

 

iframeなどの特殊なケースを除外すると、ブラウザは基本的に一つのタブで一つのスレッドが動作しています。
そしてそのスレッドで、一つのhtml(Webページ)が処理されています。

 

ブラウザには複数のタブが表示されています。つまり、ブラウザ内で複数のスレッドが並列で処理されているのです。

 

html内には、インラインのスクリプトや外部スクリプトが複数混在する可能性があります。
これらのスクリプトは、一つのスレッドで順番に処理されます。

 

またリンク上にマウスを置いた時の色変更やスクロールした時の描画処理なども、同じスレッドでおこなっています。
そのため、JavaScriptコード実行中は描画ができません。
JavaScriptの処理時間が非常に長いと、見かけ上ブラウザが停止してるような印象をユーザーに与えてしまうのです。

 

タブ一つでスレッド一つ

 

そのためJavaScriptの処理は、できる限り短くする工夫が必要です。

 

Web Worker APIで並列処理

 

描画を長時間ブロックしてしまう処理を実行しなければならないときは、Web Worker APIを利用を検討してください。

 

Web Worker APIは、作成元のスレッドと相互に通信できるサブスレッドを作成します。
つまり二つのJavaScriptコードを並列して処理できるようになります。
(方法はこちら)

 

時間がかかる処理をサブスレッドで動作させると、Webページの描画ブロックを回避できるのです。

 

■Web Worker APIでは、サブスレッドをワーカースレッドと呼んでいます。
ワーカースレッドは複数作成できます。

 

■ワーカースレッドはバックグラウンドで動作するスレッドです。
タブは生成されません。

 

ただし各スレッドは独立していて、他のスレッドのデータに直接アクセスすることはできません。
そのため、ワーカーからメインスレッドのDOMを操作するなどの処理はできません。

 

メインとなるスレッドのJavaScriptはグローバルオブジェクトとしてWindowオブジェクトを持っています。
しかしワーカースレッドは、Windowオブジェクトではなく独自のオブジェクトを持っています。
そのため、使用できない機能があります。

 

 

メッセージの送受信は非同期

 

スレッド間の通信は、メッセージの送受信でおこないます。
メッセージを受け取った側は、イベントループでメッセージがあるかどうかが確認します。
つまり、JavaScriptのコード実行中はメッセージを受け取ることができません。

 

通知してから相手が受け取るまで、タイムラグがあることを考慮しておく必要があるのです。

 

メッセージの送受信

 

これは、マウスボタンのクリックやAjaxの応答待ちと同じです。

 

マウスのボタンが押されたかどうかはOSが管理しています。
これは別のスレッドです。
Ajaxでサーバーとの通信を行っているのは、OS上の別のスレッドです。

 

ボタンが押されたり、サーバーからのデーを受信が終わるとそれぞれのスレッドからタブに通知され、最終的にメッセージとして処理されます。

 

これらはJavaScriptでは非同期処理と呼ばれています。
ワーカースレッドとの通信も、同様に非同期なのです。

 

 

ワーカースレッドには二つのタイプがある

 

Web Worker APIは『専用ワーカー』『共有ワーカー』という二つのタイプのワーカースレッドを作成することができます。

 

専用ワーカー

 

専用ワーカーは、作成元のスレッドからのみ通信可能なワーカースレッドです。

 

専用ワーカー

 

共有ワーカー

 

共有ワーカーは、作成元以外からも通信可能なワーカースレッドです。

 

共有ワーカー

 

並列処理の基本コード

 

Web Worker APIを使用した並列処理の流れは、細かく説明するよりもコードを見た方が分かりやすいです。

 

専用ワーカーのコード

 

専用ワーカー

 

次のコードはワーカースレッドを作成して、メッセージを送受信しています。
このコードはhtmlのscriptタグ内に記述してもいいですし、外部ファイルでも大丈夫です。

 

メインスレッドのコード

 


    // ①専用ワーカーの作成
const worker = new Worker( "worker.js" );

    // ②専用ワーカーにメッセージを送信
worker.postMessage(1000);

    // ③専用ワーカーからの通知を受信
worker.addEventListener( "message" , m =>{
    console.log( m.data );
});

    // ④専用ワーカーからのエラー通知を受信
worker.addEventListener( "error" , e =>{
    console.log( e.message );
});

    // ⑤専用ワーカーを終了する
// worker.terminate();

 

次のコードは専用ワーカー側です。

 

専用ワーカーのコード

 

worker.js


    // ①メインスレッドからの通知を受信
self.addEventListener( "message" , m =>{
    if( !isNumber( m.data ) ) throw new Error("数値ではない");
    const data = m.data;
    self.postMessage( data * 2 );
});
   // ②スクリプトファイルの読み込み
self.importScripts("isnumber.js");

 

isnumber.js


const isNumber = v => typeof v === "number";

 

これだけで、並列処理が実現できます。
とても簡単ですね。

 

一応、各処理の解説をしておきます。

 

メインスレッドのコード

 

①const worker = new Worker( "worker.js" );

 

外部スクリプトファイルのパスを引数として、Workerオブジェクトのインスタンスを作成します。
これだけで、自動的にワーカースレッドが作成されます。

 

例ではworker.jsを指定しているので、専用ワーカーのコードはworker.jsという名前で保存されている必要があります。

 

②worker.postMessage(1000);

 

作成したインスタンスのpostMessageメソッドを呼び出すことで、ワーカースレッドにメッセージを送信できます。
引数で指定した値が、ワーカースレッドに渡されます。
詳しくは、スレッド間のデータの転送についてをご覧ください。

 

③worker.addEventListener( "message" , m =>{console.log( m.data );});

 

"message"イベントのリスナーを登録することで、ワーカーからのメッセージを受信できます。
リスナーは、MessageEventオブジェクトを受け取ります。
このオブジェクトのdataプロパティに、ワーカーから送信されたデータがセットされます。

 

addEventListenerの他に、onmessageでもメッセージを受信できます。

 

worker.onmessage = e => { }

 

④worker.addEventListener( "error" , e =>{console.log( e.message );});

 

ワーカーで発生した例外エラーは、"error"イベントで受信できます。
リスナーは、ErrorEventオブジェクトを受け取ります。

 

addEventListenerの他に、onerrorでもメッセージを受信できます。

 

worker.onerror = e => { }

 

⑤worker.terminate();

 

terminateメソッドを呼び出すと、ワーカースレッドが終了します。
上記のコードのタイミングで呼び出すと、ワーカーからのメッセージを受信する前に終了するので、コメントになっています。

 

専用ワーカースレッドのコード

 

専用ワーカースレッドのグローバルオブジェクトは、DedicatedWorkerGlobalScopeという名前のオブジェクトです。
そして、コード例のselfは、グローバルオブジェクトへの参照です。

 

つまり次のように、selfがなくても動作します。

 

addEventListener( "message" , m =>{
    if( !isNumber( m.data ) ) throw new Error("数値ではない");
    const data = m.data;
    postMessage( data * 2 );
});

 

データの送受信は、メインスレッドと同様にaddEventListenerpostMessageでおこないます。

 

また、ワーカースレッドでエラーがスローされると、メインスレッドにエラー通知されます。

 

②self.importScripts("isnumber.js");

 

この行は、ワーカー側で必須というわけではありません。
参考として記述してあります。

 

importScriptsは、ワーカーに外部スクリプトを読み込むメソッドです。
引数の羅列で、複数のスクリプトを読み込むことができます。

 

self.importScripts("isnumber.js","a.js","b.js");

 

モジュールを読み込むimport文とは異なり、スクリプトの内容がそのまま読み込まれ実行されます。

 

 

共有ワーカーのコード

 

共有ワーカー

 

次のコードは共有ワーカースレッドが存在しなければ作成して、接続します。
存在していればそのスレッドに、接続します。

 

その後、メッセージを送受信しています。

 

このコードはhtmlのscriptタグ内に記述してもいいですし、外部ファイルでも大丈夫です。

 

メインスレッドのコード

 


    // ①共有ワーカーの作成または存在確認
const worker = new SharedWorker("shared-worker.js");

    // ②共有ワーカーからの通知を受信
worker.port.addEventListener( "message" , m =>{
    console.log( m.data );
});

  // ③共有ワーカーからのエラー通知を受信
worker.addEventListener( "error" , e =>{
    console.log( e.message );
});

  // ④通信開始
worker.port.start();

  // ⑤共有ワーカーにメッセージを送信
worker.port.postMessage(1);

  // ⑥共有ワーカーとの接続解除
// worker.port.close();

 

次のコードは共有ワーカー側です。

 

共有ワーカーのコード

 

shared-worker.js


self.addEventListener( "connect" , e =>{

    const port = e.ports[0];
    port.addEventListener( "message" , m =>{
        if( !isNumber( m.data ) ) throw new Error("数値ではない");
        const data = m.data;
        port.postMessage( data * 2 );
    });
    port.start();
});

self.importScripts("isnumber.js");

 

isnumber.js


const isNumber = v => typeof v === "number";

共有ワーカーは、できる限り短い時間で処理をおこなうべきです。
今行っている処理が終わるまで他のスレッド(Webページ)からの要求に答えることができません。
要求を行ったページは応答待ちとなり、動きが遅い印象をユーザーに与える可能性があるからです。

 

同様に複数の処理を詰め込むのも、応答が遅くなる原因になります。

 

各処理の解説をしておきます。

 

メインスレッドのコード

 

①const worker = new SharedWorker("shared-worker.js");

 

外部スクリプトファイルのパスを引数として、SharedWorkerオブジェクトのインスタンスを作成します。
このとき、共有ワーカースレッドが存在していなければ作成されます。

 

第二引数で、共有ワーカーに名前を付けることができます。

 

const workerA = new SharedWorker("shared-worker.js","workerA");
const workerB = new SharedWorker("shared-worker.js","workerB");

 

こうすることで、同じファイルでも異なる共有ワーカーとして使用できます。

 

インスタンスを作成した時点では、共有ワーカーと接続されていません。

 

②worker.port.addEventListener( "message" , m =>{console.log( m.data );});

 

専用ワーカーと異なり共有ワーカーからのメッセージ受信は、portプロパティにイベント登録します。
リスナーは、MessageEventオブジェクトを受け取ります。
このオブジェクトのdataプロパティに、ワーカーから送信されたデータがセットされます。

 

この時点では、共有ワーカーと接続されていません。

 

addEventListenerの他に、onmessageでもメッセージを受信できます。

 

worker.onmessage = e => { }

 

onmessageプロパティをセットすると、共有ワーカーと接続されます。
そのため、⑤の処理は必要ありません。

 

 

③worker.addEventListener( "error" , e =>{console.log( e.message );});

 

共有ワーカーで発生した例外エラーは、"error"イベントで受信できます。
リスナーは、ErrorEventオブジェクトを受け取ります。

 

②と異なり、portプロパティではなく、SharedWorkerのインスタンスにイベント登録している点に注意してください。

 

addEventListenerの他に、onerrorでもメッセージを受信できます。

 

worker.onerror = e => { }

 

④worker.port.start();

 

共有ワーカーに接続して、"connect"イベントを送信します。

 

メッセージの受信をonmessageプロパティで設定している場合は、この処理は必要ありません。

 

⑤worker.port.postMessage(1);

 

共有ワーカースレッドにメッセージを送信します。
詳しくは、スレッド間のデータの転送についてをご覧ください。

 

⑥worker.port.close();

 

共有ワーカースレッドとの接続を終了します。
上記のコードのタイミングで呼び出すと、ワーカーからのメッセージを受信する前に終了するので、コメントになっています。

 

 

共有ワーカースレッドのコード

 

共有ワーカースレッドのグローバルオブジェクトは、SharedWorkerGlobalScopeという名前のオブジェクトです。
コード例のselfは、グローバルオブジェクトへの参照です。
selfがなくても動作します。

 

self.addEventListener( "connect" , e =>{

 

メインスレッドが接続されると、"connect" イベントを受信します。

 

const port = e.ports[0];

 

"connect" イベントで受け取ったオブジェクトの、ports[0]を記憶しておきます。
メインスレッドとの通信は、このポートを介しておこないます。

 

port.addEventListener( "message" , m =>{

 

メインスレッドからのメッセージを受け取るように設定します。

 

port.postMessage( data * 2 );

 

メインスレッドにメッセージを送信します。

 

port.start();

 

メインスレッドとの通信を開始します。

 

ワーカーのデバッグ

 

ブラウザの開発ツールでワーカーをデバッグする方法をお伝えします。

 

 

専用ワーカーのデバッグ

 

専用ワーカーは、メインスレッドと同じ画面でデバッグできます。

 

Google Chrome

 

Sorcesタブの左側に、スクリプトファイルの名前一覧があるので、選択してブレークポイント等を設定してください。

 

専用ワーカーのデバッグ(Chrome)

 

 

Firefox

 

デバッガ―タブの左側に、スクリプトファイルの名前一覧があるので、選択してブレークポイント等を設定してください。

 

専用ワーカーのデバッグ(Firefox)

 

共有ワーカーのデバッグ

 

共有ワーカーは、別のデバッグ画面を開く必要があります。

 

 

Google Chrome

 

新しいタブ開き、アドレスバーに『chrome://inspect/#workers』と入力します。
次のような画面が表示されます。

 

chrome://inspect/#workers

 

『inspect』をクリックすると、共有ワーカー用の開発ツールが表示されます。

 

ローカルファイルでデバッグしたところ、ブレークポイントで停止しませんでした。
(バージョン: 95.0.4638.54にて確認)
Webサーバーが必要かもしれません。

 

Firefox

 

新しいタブ開き、アドレスバーに『about:debugging#/runtime/this-firefox』と入力します。
拡張機能等の一覧が表示されるので、スクロールして『Shared Worker』を探します。

 

about:debugging#/runtime/this-firefox

 

『調査』をクリックすると、共有ワーカー用のデバッガ―が表示されます。

 

接続数が一つのときメインスレッド(Webページ)を再表示すると共有ワーカーが削除されるため、デバッグできません。
同じページを同時に複数表示してからデバッグする必要があります。
(バージョン:93.0 にて確認)

ワーカ用の外部スクリプトパス

 

 

 

サブワーカーのパス

 

ワーカースレッドは、メインスレッドと同じようにワーカーを作成することができます。

 

次の例は、メインスレッドで作成したワーカースレッドでサブとなるワーカースレッドを作成しています。

 

 

メインスレッド

 


const worker = new Worker( "/js/worker.js");
worker.postMessage(1);

worker.addEventListener( "message" , m =>{
    console.log( m.data );
});

 

ワーカースレッド

 


const childWorker = new Worker( "worker2.js");
childWorker.addEventListener( "message" , m =>{
    postMessage( m.data );
});

addEventListener( "message" , m =>{
    childWorker.postMessage( m.data * 2 );
});

 

サブワーカースレッド

 


addEventListener( "message" , m =>{
    postMessage( m.data * 2 );
});

 

このとき、サブワーカーのスクリプトファイルは、呼び出し元のスクリプトファイルが基準となります。
ワーカーのスクリプトファイルは /js/worker.js なので、サブは /js/worker2.js が参照されます。

 

なお、次のように "/" が記述されると、ドメイン/worker2.js が読み込まれます。

 

const childWorker = new Worker( "/worker2.js");

 

異なるドメインからのワーカー読み込み

 

ワーカーとして読み込むスクリプトファイルは、クロスオリジンの制約を受けます。
つまり、ドメインが異なるとエラーになってしまいます。

 

どうしても読み込みたい場合は、つぎのようにblobを使用するのも一つの方法です。

 

異なるドメインスクリプトの読み込み

 


const crossWorker = url => new Worker(
        URL.createObjectURL(new Blob( [`importScripts("${url}")` ], {
            type: 'text/javascript'
     })));

const worker = crossWorker( "異なるドメインURL" );

 

次のような処理をしています。

 

  1. importScripts( "異なるドメインURL" ) というスクリプトを返すURLをblobで作成
  2. 作成したURLで、ワーカーを作成
  3. ワーカーのスクリプト(importScripts( "異なるドメインURL" ))が実行される
  4. importScripts()がスクリプトを読み込み、そのスクリプトがワーカー内で実行される

 

importScriptsはクロスオリジンの制約を受けないことを利用した処理になっています。

 

ただし、異なるドメインのスクリプト内で次のように importScripts や サブワーカーを使用する場合は注意が必要です。

 

異なるドメインのスクリプト

 

  ・・・・メインスレッドとの通信しょり
  ・・・・何らかのコード

    // 異なるドメイン/worker2.jsでサブワーカーを作成 
const childWorker = new Worker( "worker2.js");

   // 異なるドメイン/isnumber.jsを読み込み
self.importScripts("isnumber.js");

 

blobを使用するとURLの起点がblobとなり、内部で読み込むスクリプトファイルのURLもそれに応じたものになります。
上記の例では異なるドメイン/worker2.js の読み込みを想定していますが、別のURLを参照してしまい、読み込みに失敗します。
importScriptsも同様です。

 

importScriptsはフルパスで指定して、サブワーカーはcrossWorker関数のような仕組みで呼び出す必要があります。
つまり、異なるドメインから呼び出されることを想定して組み込んでおく必要があります。

 

ローカルファイルでのワーカー呼び出し

 

Webサーバーを使用せずに、ローカルhtmlをブラウザ表示してスクリプトを実行したとき、ワーカースクリプトを読み込むと、ブラウザ側でエラーになります。

 

最近のブラウザは、デフォルトでファイルからの読み込みを禁止しているからです。
解除方法を別の記事でお伝えしているので、そちらをご覧ください。

 

 

スレッド間のデータの転送について

 

ワーカーにメッセージを送信するpostMessageは、二つの引数を受け付けます。

 

postMessageの構文

 

postMessage( シリアル化されるデータ , 所有権転移データ );

 

 

この二つの引数で渡されたデータは、通常の関数の引数とは異なる渡され方をします。

 

シリアル化されるデータ: 転送前にシリアル化され、転送後にデータとして復元される。

 

所有権転移データのリストシリアル化されるデータに含まれるデータで、データの所有権を転送側に移すもののリスト。

 

各引数について簡単に説明します。

 

シリアル化されるデータ

 

postMessageの最初の引数に指定されたデータは、転送前にシリアル化され、転送後にデータとして復元されます。
この作業により、転送後のデータを変更しても転送前のデータに影響を与えないようになります。

 

ここでのシリアル化は、元のデータから必要な情報のみを抜き出して設計図を作成するような作業です。
転送後は、その設計図を参照して新たにデータを作成します。

 

この手法としてjsonが挙げられることがあります。
そのため文字列に変換される印象がありますが、そのようなことはありません。

 

以下は、2.7 Safe passing of structured data | HTML Living Standardの内容を解釈したものです。解釈間違い等ありましたら、ご指摘ください。

 

■プリミティブの転送

 

プリミティブは、プリミティブへの参照が転送されて、転送先の識別子と関連付けられます。これについては代入や関数への引数渡しと同じです。

 

シリアル化によりプリミティブの実体も複製されるようなイメージがありますが、メインスレッドとワーカーで同じものが参照されます。
プリミティブは、一度作成されるとガベージコレクションで削除されるまで値が上書きされることがありません。
そのため、実際のデータを複製する必要はないのです。

 

jsの変数代入について

 

シンボルは転送できません。転送データにシンボルを含んでいるとエラーになります。

 

■オブジェクトの転送

 

オブジェクトはオブジェクトが直接所持するプロパティの名前と値の一覧が作成され、それが転送されます。
プロトタイプチェーン内のプロパティは直接所持ではないので、無視されます。

 

値はプリミティブまたはオブジェクトです。

 

値がプリミティブのときは、上記の方法で転送/復元されます。
値がオブジェクトのときは、再帰的に処理されます。オブジェクトが上階層のオブジェクトを参照していて循環することも考慮した設計になっています。
プロパティに設定されている書き込み禁止属性などは転送されません。
そのため、デフォルト値となります。

 

 

なおデータ内に、関数オブジェクトを含んでいるとエラーとなります。
つまりメソッドは転送できません。

 

セッター/ゲッターはプロパティ名とゲッターから得た値が転送され、プロパティと値に復元されます。
つまり、セッター/ゲッターとしての機能は失われます。
なお、セッターのみの場合は値がundefinedになります。

 

また、JavaScriptの組み込みオブジェクトは、次のような処理をおこないます。

 

オブジェクト名処理備考
Boolean真偽データのみ転送
Number数値データのみ転送
BigInt長整数データのみ転送
String文字列データのみ転送
Date日付データのみ転送
RegExp正規表現マッチデータと元となるソースおよびフラグのみ転送lastIndexは初期値になる
ArrayBuffer新たなデータブロックにデータをコピーして、

コピーしたデータとデータサイズのみ転送

SharedArrayBufferデータとデータサイズのみ転送スレッド間で同じデータにアクセス可能
DataViewデータとバイト数とバイトオフセットを転送一つのArrayBufferを

複数のDataViewまたは
TypedArrayが参照している場合でも、
同じバッファーを参照するように処理されます。
バッファーが複数作成されることはありません。

TypedArrayデータとバイト数とバイトオフセットとアレイ長のみ転送
MapMapに保持されているデータのみ転送
SetSetに保持されているデータのみ転送
Errorエラー名とメッセージのみ転送
Arraylengthとプロパティ名/値のリストを転送配列以外のプロパティも転送されます

 

上記でデータとして関数オブジェクトが含まれている場合、エラーになります。

 

上記以外の組み込みオブジェクト(PromiseWeakMapなど)は、一部を除いてエラーになります。

 

SharedArrayBufferについてはこちらで簡単に解説しています。
【JavaScript】 SharedArrayBufferの使い方とブラウザでの制限

 

所有権転移データのリスト

 

postMessageの2番目の引数は、シリアル化されるデータに含まれるオブジェクトを、配列で指定します。
これは省略可能です。

 

指定したオブジェクトは、シリアル化されずに転送されます。

 

このときオブジェクトの所有権が送信先のスレッドに移り、転送元ではそのオブジェクトを使用できなくなります。

 

ただし全てのオブジェクトが所有権転移できるわけではありません。
指定可能なのは transferableオブジェクトと呼ばれていて、こちらに適用条件が書かれています。

 

主にArrayBufferを指定することを目的としていて、バッファーデータの複製時間を削減することで、高速に転送することができます。
なお転送元には、サイズ0で初期化されたArrayBufferがセットされます。

 

その他のオブジェクトは、具体例が示されていないため、自分で調査して使用する必要があります。

 

ArrayBufferを所有権転移で転送する例を幾つか挙げてみます。

 

まずは単純にArrayBufferを転送する例です。

 

ArrayBufferを所有権転移

 


const aBuffer = new ArrayBuffer(10);
worker.postMessage( aBuffer , [ aBufferr ] );
console.log( aBuffer.byteLength ); // 0

 

次はTypedArrayです。
TypedArrayはbufferでArrayBufferを参照できます。

 

TypedArrayを所有権転移

 


const u8 = new Uint8Array(1);
worker.postMessage( u8 , [ u8.buffer ] );
console.log( u8.byteLength );

 

次は複数のArrayBufferに関連したTypedArrayを転送しています。

 

複数のArrayBufferを所有権転移

 


const ar = new ArrayBuffer(2);
const o = {
    ar1 : new Uint8Array( ar ),
    ar2: new Uint16Array( ar ),
    ar3: new Uint32Array( 2 ),
};
worker.postMessage( o , [ ar , o.ar3.buffer ]);
console.log( o.ar1.byteLength , o.ar2.byteLength , o.ar3.byteLength); // 0 0 0

 

ArrayBufferとTypedArrayについては、次の記事を読んでみてください。
【JavaScript】 ArrayBufferとTypedArray-メモリを確保してアクセス

Promise化のヒント

 

Workerからの応答はメッセージで受け取るので、非同期です。
これをasync/awaitを使って、同期的に受け取る方法を考えてみます。

 

async/awaitを使用するにはメッセージ受け取り時に解決するようなPromiseを構築する必要があります。

 

例えば次のように、ワーカーからの応答受信部分をPromiseのコールバックに含めてしまう方法が考えられます。

 

単純なPromise化

 

メインスレッド

 


const workerWait = message =>{
    return new Promise( resolve => {
        const func = m => {
            worker.removeEventListener("message",func);
            resolve( m.data );
        }
        worker.addEventListener( "message" , func );
        worker.postMessage(message);
    });
};
(async ()=>{
    const t = await workerWait( 300 );
    console.log( t );
})();

 

ワーカースレッド

 


addEventListener( "message" , m =>{

    const data = m.data;
    postMessage( data * 100 );
});

 

 

addEventListenerは呼び出すたびに追加されていくので、removeEventListenerで削除しています。

 

このコードは一見うまく動作するように見えますが、大きな問題があります。

 

例えば次のようにワーカースレッドと、メイン側のworkerWait呼び出しを変更してみます。

 

問題点の検証

 

メインスレッド

 


(async ()=>{
    const result = await workerWait(3000);
    console.log( `1:${ result } 経過しました` );
})();

(async ()=>{
    const result = await workerWait(1000);
    console.log( `2:${ result } 経過しました` );
})();

 

ワーカースレッド

 


addEventListener( "message" , m =>{

    const data = m.data;
    setTimeout( ()=>postMessage(data) , data );
});

 

 

ワーカースレッド側でタイマー待ちをして、時間が経過したらメインに通知しています。
メイン側では異なる待ち時間で、二つのasync関数が実行されています。

 

この結果は、次のようになります。

 

1:1000 経過しました // 期待した結果は "1:3000 経過しました"
2:1000 経過しました

 

期待した結果になっていません。

 

理由は単純です。

 

  1. async関数で中断するのは、関数内のみ

    =>一番目のasyncのworkerWait結果待ちで関数を抜けて、次のasyncが実行された

  2. addEventListenerで登録された関数は順番に処理される

    =>二番目に対するワーカーからの通知を、一番目も受信して処理している

 

解決方法も単純です。
ワーカーからの通知が、どのPromiseからのものか判定できればいいのです。

 

ワーカーからの応答をPromise化

 

メインスレッド

 


const workerPromise = ((worker)=>{
    const workerBuffer = [];

    worker.addEventListener( "message" , m =>{
        const data = m.data;
        workerBuffer[data.promiseIndex](data.message);
        workerBuffer[data.promiseIndex]=undefined;
    });

    return  message =>{
            const promise = new Promise( r=>{
                workerBuffer.push( r );
            });
            worker.postMessage({promiseIndex:workerBuffer.length -1,message:message});
            return promise;
        };
})(worker);

(async ()=>{
    const result = await workerPromise(3000);
    console.log( `1:${ result } 経過しました` );
})();

(async ()=>{
    const result = await workerPromise(1000);
    console.log( `2:${ result } 経過しました` );
})();

 

ワーカースレッド

 

addEventListener( "message" , m =>{
    const data = m.data;
    setTimeout( ()=>postMessage(data) , data.message );
});

 

この結果は、次のようになります。

 

2:1000 経過しました
1:3000 経過しました

 

このコードは、addEventListenerは一回だけ実行しています。
この方が効率がいいですね。

 

Promiseのインスタンス作成時にresolveを配列に格納しておき、その配列のインデックスをワーカーに渡しています。
ワーカーはメインへの通知時に、そのインデックスを含めたデータを返します。
メイン側は受け取ったインデックスから、格納しておいたresolveを取り出して実行します。

 

単純ですね。
実際に運用するなら、使用済みのインデックスを検索する必要があるかもしれません。

 

もっと良い方法があると思うので、考えてみてください。

 

ちなみにスレッド間で転送されたオブジェクトは、新たなオブジェクトとして複製されます。
そのためresolveの記憶にWeakMapは使用できません。

 

最後までお読みいただきありがとうございます

記事の内容について

こんにちはけーちゃんです。
説明するのって難しいですね。


「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

【お願い】

お願い

■このページのURL


■このページのタイトル


■リンクタグ


※リンクして頂いた方でご希望者には貴サイトの紹介記事を作成してリンクを設置します。
サイト上部の問い合わせよりご連絡ください。
ただしサイトのジャンルによっては、お断りさせていただくことがあります。