Wasm Workers API

Wasm Workers APIにより、C/C++コードはWeb Workersと共有WebAssembly.Memory(SharedArrayBuffer)を活用して、直接的なWebライクなプログラミングAPIを介してマルチスレッドプログラムを構築できます。

簡単な例

#include <emscripten/wasm_worker.h>
#include <stdio.h>

void run_in_worker()
{
  printf("Hello from Wasm Worker!\n");
}

int main()
{
  emscripten_wasm_worker_t worker = emscripten_malloc_wasm_worker(/*stackSize: */1024);
  emscripten_wasm_worker_post_function_v(worker, run_in_worker);
}

コンパイルとリンクの両方のステップでEmscriptenフラグ-sWASM_WORKERSを渡してコードをビルドします。このコード例は、メインブラウザスレッド上に新しいワーカーを作成し、同じWebAssembly.ModuleとWebAssembly.Memoryオブジェクトを共有します。次に、postMessage()がワーカーに渡され、文字列を出力する関数run_in_worker()を実行するように要求します。

ワーカーを作成する際のメモリ割り当ての配置を明示的に制御するには、emscripten_create_wasm_worker()関数を使用します。この関数は、ワーカーのスタックとTLSデータの両方を保持するのに十分な大きさのメモリ領域を取ります。プログラムのTLSデータに必要なスペースをランタイムで調べるには、__builtin_wasm_tls_size()を使用できます。

はじめに

WebAssemblyプログラムでは、アプリケーションの状態を含むMemoryオブジェクトを複数のワーカー間で共有できます。これにより、複数のワーカー間でデータ状態を同期的に共有する(共有状態マルチスレッディング)ための直接的で高性能なアクセスが可能になります(明示的な配慮がない場合、競合状態が発生する可能性があります)。

Emscriptenは、このWeb機能を活用するための2つのマルチスレッディングAPIをサポートしています。
  • POSIXスレッド(Pthreads)API、および

  • Wasm Workers API。

Pthreads APIはネイティブCプログラミングとPOSIX標準で長い歴史がありますが、Wasm Workers APIはEmscriptenコンパイラ固有のものです。

これらの2つのAPIはほぼ同じ機能セットを提供しますが、重要な違いがあります。このドキュメントでは、これらの違いを説明し、どのAPIをターゲットにするかを決定するのに役立つ情報を提供します。

PthreadsとWasm Workers:どちらを使用するか?

これらの2つのマルチスレッディングAPIの対象となるオーディエンスとユースケースはわずかに異なります。

Pthreads APIは、移植性とクロスプラットフォーム互換性に重点を置いています。このAPIは、移植性が最も重要なシナリオ、たとえば、ネイティブLinux x64実行ファイルとEmscripten WebAssemblyベースのWebサイトの両方をビルドする場合などに最適です。

EmscriptenのPthreads APIは、ネイティブPthreadsプラットフォームが既に提供している互換性と機能を注意深くエミュレートしようとします。これにより、大規模なC/C++コードベースをWebAssemblyに移植しやすくなります。

一方、Wasm Workers APIは、Web上に存在するWebマルチスレッディングプリミティブへの「直接マッピング」を提供することを目指しています。アプリケーションがWebAssemblyのみをターゲットとして開発されており、移植性が問題にならない場合は、Wasm Workersを使用することで、より単純なコンパイル済み出力、複雑さの軽減、コードサイズの縮小、パフォーマンスの向上などの大きなメリットが得られます。

ただし、このメリットは必ずしも明らかなものではありません。Pthreads APIは同期的なC/C++言語から使用できるよう設計されていますが、Web Workersは非同期的なJavaScriptから使用できるよう設計されています。WebAssembly C/C++プログラムは、その中間点に位置することがあります。

PthreadsとWasm Workersにはいくつかの類似点があります。

  • どちらもemscripten_atomic_* Atomics APIを使用できます。

  • どちらもGCC __sync_* Atomics APIを使用できます。

  • どちらもC11およびC++11 Atomics APIを使用できます。

  • どちらの種類のスレッドにもローカルスタックがあります。

  • どちらの種類のスレッドも、thread_local(C++11)、_Thread_local(C11)、__thread(GNU11)キーワードを介してスレッドローカルストレージ(TLS)サポートを備えています。

  • どちらの種類のスレッドも、明示的にリンクされたWasmグローバルを介してTLSをサポートします(例としてtest/wasm_worker/wasm_worker_tls_wasm_assembly.c/.Sのコードを参照してください)。

  • どちらの種類のスレッドにも、スレッドIDの概念があります(pthreadsの場合はpthread_self()、Wasm Workersの場合はemscripten_wasm_worker_self_id())。

  • どちらの種類のスレッドも、イベントベースと無限ループのプログラミングモデルを実行できます。

  • どちらもEM_ASMおよびEM_JS APIを使用して、呼び出し元のスレッドでJSコードを実行できます。

  • どちらも--js-libraryディレクティブでリンクされたJSライブラリ関数呼び出して、呼び出し元のスレッドでJSコードを実行できます。

  • PthreadsとWasm Workersのどちらも、-sSINGLE_FILEリンカーフラグと併用することはできません。

ただし、違いはより顕著です。

PthreadsはJS関数をプロキシできます

PthreadsのみがMAIN_THREAD_EM_ASM*()およびMAIN_THREAD_ASYNC_EM_ASM()関数と、JSライブラリ内のfoo__proxy: 'sync'/'async'プロキシディレクティブを使用できます。

一方、Wasm Workersは組み込みのJS関数プロキシ機能を提供しません。Wasm WorkersでJS関数をプロキシするには、その関数のアドレスをemscripten_wasm_worker_post_function_* APIに明示的に渡す必要があります。

ワーカー内から投稿された関数の完了を同期的に待機する必要がある場合は、emscripten_wasm_worker_*()スレッド同期関数のいずれかを使用して、呼び出し元のスレッドを、呼び出し側が操作を完了するまでスリープ状態にします。

Wasm Workersはできません。

Pthreadsにはキャンセルポイントがあります

パフォーマンスとコードサイズの犠牲になりますが、pthreadsは**POSIXキャンセルポイント**(pthread_cancel()pthread_testcancel())の概念を実装しています。

Wasm Workersはその概念を有効化しないため、より軽量で高性能です。

Pthreadsは同期的に起動される可能性があります - Wasm Workersは常に非同期的に起動されます

新しいワーカーの作成は遅くなる可能性があります。JavaScriptでワーカーを生成することは非同期操作です。同期的なpthreadの起動をサポートするため(必要なアプリケーションの場合)およびスレッドの起動パフォーマンスを向上させるため、pthreadsはキャッシュされたEmscriptenランタイムによって管理されるワーカープールにホストされます。

Wasm Workersはこの概念を省略するため、Wasm Workersは常に非同期的に起動されます。Wasm Workerの起動を検出する必要がある場合は、ワーカーとその作成者の間でping-pong関数と返信ペアを手動でポストします。新しいスレッドをすばやく起動する必要がある場合は、Wasm Workersのプールを自分で管理することを検討してください。

Pthreadのトポロジーはフラットだが、Wasm Workerは階層構造である

ウェブ上では、Workerが自身のチャイルドWorkerを生成すると、メインスレッドが直接アクセスできないネストされたWorker階層が作成されます。この種のトポロジーに起因する移植性の問題を回避するために、pthreadは内部的にWorker生成チェーンをフラット化し、メインブラウザスレッドだけがスレッドを生成するようにします。

Wasm Workerはこのようなトポロジーのフラット化を実装しておらず、Wasm Worker内でWasm Workerを作成すると、ネストされたWorker階層が生成されます。Wasm Worker内からWasm Workerを作成する必要がある場合は、どのような階層が必要か、そして必要に応じて、Workerの作成をメインスレッドにポストすることで階層を手動でフラット化することを検討してください。

ネストされたWorkerのサポートはブラウザによって異なります。2022年2月現在、ネストされたWorkerはSafariではサポートされていませんポリフィルはこちらを参照してください。

PthreadはWasm Worker同期APIを使用できますが、逆はできません

emscripten/wasm_worker.h(emscripten_lock_*emscripten_semaphore_*emscripten_condvar_*)で提供されるマルチスレッド同期プリミティブは、必要に応じてpthread内から自由に呼び出すことができますが、Wasm Workerはpthreadランタイムがないため、Pthread API(pthread_mutex_*pthread_cond_pthread_rwlock_*など)の同期機能を使用できません。

Pthreadには「スレッドメイン」関数とatexitハンドラがある

pthreadの起動/実行モデルは、指定されたスレッドエントリポイント関数の実行を開始することです。その関数が終了すると、pthreadも(デフォルトでは)終了し、そのpthreadをホストしているWorkerは、別のスレッドが作成されるのを待つためにWorkerプールに戻ります。

一方、Wasm Workerは、新しく作成されたWorkerがイベントループ内でアイドル状態になり、関数へのポストを待機する直接的なウェブのようなモデルを実装しています。これらの関数が終了すると、Workerはイベントループに戻り、実行する関数(またはワーカースコープのウェブイベント)をさらに受信するのを待ちます。Wasm Workerはemscripten_terminate_wasm_worker(worker_id)またはemscripten_terminate_all_wasm_workers()を呼び出すことでのみ終了します。

Pthreadではpthread_atexitを使用してスレッド終了ハンドラを登録できます。これはスレッドが終了したときに呼び出されます。Wasm Workerにはこの概念はありません。

Pthreadにはスレッドごとの着信プロキシメッセージキューがあるが、Wasm Workerにはない

他のスレッドでコードを柔軟に同期実行できるようにし、たとえばMEMFSファイルシステムやオフスクリーンフレームバッファ(WorkerからエミュレートされたWebGL)機能のサポートAPIを実装するために、メインブラウザスレッドと各pthreadには、システムバックエンドの「プロキシメッセージキュー」があり、メッセージを受信します。

これにより、ユーザーコードはemscripten/threading.hからAPI関数(emscripten_sync_run_in_main_runtime_thread()emscripten_async_run_in_main_runtime_thread()emscripten_dispatch_to_thread()など)を呼び出して、プロキシされた呼び出しを実行できます。

Wasm Workerはこの機能を提供しません。必要に応じて、このようなメッセージングは、通常のマルチスレッド同期プログラミング手法(ミューテックス、futex、セマフォなど)を使用してユーザーが手動で実装する必要があります。

Pthreadはウォールクロック時間を同期する

Pthreadが提供するもう1つの移植性支援エミュレーション機能は、emscripten_get_now()によって返される時間値が、すべてのスレッド間で共通の時間ベースに同期されることです。

Wasm Workerはこの概念を省略しており、Wasm Workerでの高性能タイミングにはemscripten_performance_now()関数を使用し、結果の値をWorker間で比較したり、手動で同期したりしないことをお勧めします。

入力イベントAPIバックプロキシはpthreadのみに対応

emscripten/html5.hで提供されるマルチスレッド入力APIは、pthread APIでのみ機能します。emscripten_set_*_callback_on_thread()関数のいずれかを呼び出す場合、受信したイベントの受信者としてターゲットpthreadを選択できます。

Wasm Workerでは、必要に応じて、メインブラウザスレッドからWasm Workerへのイベントの「バックプロキシ」を、たとえばemscripten_wasm_worker_post_function_*()APIファミリを使用して手動で実装する必要があります。

ただし、入力イベントのバックプロキシには、フルスクリーンリクエスト、ポインタロック、オーディオ再生の再開など、セキュリティに敏感な操作を妨げるという欠点があることに注意してください。これは、入力イベントの処理が、初期操作を実行するイベントコールバックコンテキストから切り離されているためです。

Pthreadとemscripten_lockの実装の違い

pthread_mutex_*のミューテックス実装には、いくつかの異なる作成オプションがあり、その1つが「再帰的」ミューテックスです。

emscripten_lock_*APIによって実装されるロックは再帰的ではありません(オプションも提供されません)。

Pthreadはまた、あるスレッドが別のスレッドが所有するロックを解放しないというプログラミングエラーに対するプログラミングガードを提供します。emscripten_lock_*APIはロックの所有権を追跡しません。

メモリ要件

Pthreadは動的メモリ割り当てに固定の依存関係があり、スレッド固有のデータ、スタック、TLSスロットを割り当てるためにmallocfreeを呼び出します。

ヘルパー関数emscripten_malloc_wasm_worker()を除いて、Wasm Workerは動的メモリアロケータに依存しません。メモリの割り当てニーズは、Worker作成時に呼び出し元によって満たされ、必要に応じて静的に配置できます。

生成されたコードサイズ

pthreadからのディスクサイズのオーバーヘッドは、数百KBのオーダーです。一方、Wasm Workerランタイムは、ディスク上の数百バイトという小さな展開のために最適化されています。

APIの違い

PthreadとWasm Workerで利用可能な異なるAPIについてさらに理解するには、次の表を参照してください。

機能 Pthread Wasm Worker
スレッドの終了 スレッド呼び出し
pthread_exit(status)
またはメインスレッド呼び出し
pthread_kill(code)
Workerは自身を終了できません。親スレッドは呼び出して終了します。
emscripten_terminate_wasm_worker(worker)
スレッドスタック pthread_attr_t構造で指定します。 関数を使用してスレッドスタック領域を明示的に管理するか、
emscripten_create_wasm_worker_*_tls()
または
APIを使用してスタック+TLS領域を自動的に割り当てます。
emscripten_malloc_wasm_worker()
API。
スレッドローカルストレージ(TLS) 透過的にサポートされています。 明示的にサポートするか、
emscripten_create_wasm_worker_*_tls()
または
を介して自動的にサポートします。
emscripten_malloc_wasm_worker()
API。
スレッドID pthreadを作成すると、そのIDが取得されます。呼び出しスレッドのIDを取得するには、
pthread_self()
を呼び出します。
Workerを作成すると、そのIDが取得されます。呼び出しスレッドのIDを取得するには、
emscripten_wasm_worker_self_id()
を呼び出します。
高解像度タイマー ``emscripten_get_now()`` ``emscripten_performance_now()``
メインスレッドでの同期ブロッキング 同期プリミティブは内部的にビジーウェイトループにフォールバックします。 明示的なスピン対スリープ同期プリミティブ。
Futex API
emscripten_futex_wait
emscripten_futex_wake
emscripten/threading.h内
emscripten_atomic_wait_u32
emscripten_atomic_wait_u64
emscripten_atomic_notify
emscripten/atomic.h内
非同期futex待ち N/A
emscripten_atomic_wait_async()
emscripten_*_async_acquire()
ただし、これらは難しい足かせです。WebAssembly/threads issue #176を参照してください。
C/C++関数プロキシ 他のスレッドに関数呼び出しをプロキシするためのemscripten/threading.h API。 emscripten_wasm_worker_post_function_*() APIを使用して、他のスレッドに関数をメッセージングします。これらのメッセージは、プロキシキューセマンティクスではなく、イベントキューセマンティクスに従います。
ビルドフラグ -pthreadでコンパイルおよびリンクします。 -sWASM_WORKERSでコンパイルおよびリンクします。
プリプロセッサディレクティブ __EMSCRIPTEN_SHARED_MEMORY__=1と__EMSCRIPTEN_PTHREADS__=1がアクティブです。 __EMSCRIPTEN_SHARED_MEMORY__=1と__EMSCRIPTEN_WASM_WORKERS__=1がアクティブです。
JSライブラリディレクティブ USE_PTHREADSとSHARED_MEMORYがアクティブです。 USE_PTHREADS、SHARED_MEMORY、WASM_WORKERがアクティブです。
Atomics API __atomic_* API__sync_* API、またはC++11 std::atomic APIのいずれかを使用します。
非再帰ミューテックス
pthread_mutex_*
emscripten_lock_*
再帰ミューテックス
pthread_mutex_*
N/A
セマフォ N/A
emscripten_semaphore_*
条件変数
pthread_cond_*
emscripten_condvar_*
読取書き込みロック
pthread_rwlock_*
N/A
スピンロック
pthread_spin_*
emscripten_lock_busyspin*
WebGLオフスクリーンフレームバッファ
Supported with -sOFFSCREEN_FRAMEBUFFER
Not supported.

Wasm Workerのスタックサイズに関する考慮事項

Wasm Workerをインスタンス化する場合、作成されたWorkerのLLVMデータスタック用のメモリ配列を作成する必要があります。このデータスタックは一般的に、LLVMによってメモリに「スピル」されたローカル変数のみで構成されます。たとえば、大きな配列、構造体、またはメモリアドレスによって参照される他の変数を格納するためです。このスタックには、制御フロー情報は含まれません。

WebAssemblyは仮想メモリをサポートしていないため、Wasm Workerとメインスレッドの両方で定義されているLLVMデータスタックのサイズは、ランタイム時に増やすことができません。そのため、Worker(またはメインスレッド)がスタック空間を使い果たすと、プログラムの動作は未定義になります。開発中にこれらの状況を検出するには、Emscriptenリンカーフラグ-sSTACK_OVERFLOW_CHECK=2を使用して、ランタイムスタックオーバーフローチェックをプログラムコードに埋め込みます。

2つの別々の割り当てを行う必要性を回避するために、Wasm WorkerのTLSメモリは、Wasm Workerスタック空間の末尾(低メモリアドレス)に配置されます。

Wasm Workerと以前のEmscripten Worker API

Emscriptenは、emscripten.hヘッダーの一部として、2つ目のWorker APIを提供します。このWorker APIはSharedArrayBufferが登場する前に存在しており、Wasm Workers APIとは全く異なります。これらの2つのAPIの名前が似ているのは、歴史的な理由によるものです。

どちらのAPIも、メインスレッドからWeb Workerを生成することを可能にします。ただし、セマンティクスは異なります。

Worker APIを使用すると、ユーザーはカスタムURLからWeb Workerを生成できます。このURLは、Emscriptenでコンパイルされていない完全に独立したJSファイルを指し示すことができ、任意のURLからWorkerを読み込むことができます。Wasm Workersでは、カスタムURLは指定されません。Wasm Workersは常に、メインプログラムと同じWebAssembly+JavaScriptコンテキストで計算を行うWeb Workerを生成します。

Worker APIはSharedArrayBufferと統合されていないため、読み込まれたWorkerとのインタラクションは常に非同期になります。一方、Wasm WorkersはSharedArrayBuffer上に構築されており、各Wasm Workerはメインスレッドと同じWebAssemblyメモリアドレス空間を共有して計算を行います。

Worker APIとWasm Workers APIの両方で、ユーザーはpostMessage()関数呼び出しをWorkerに送信する機能が提供されます。Worker APIでは、このメッセージ送信は、メインスレッドからWorkerへの発信/開始に制限されています(`emscripten.h>内の`emscripten_call_worker()と`emscripten_worker_respond() APIを使用)。一方、Wasm Workersでは、親(所有)スレッドにpostMessage()関数呼び出しを送信することもできます。

Emscripten Worker APIで関数呼び出しを送信する場合、ターゲットWorkerのURLがEmscriptenでコンパイルされたプログラムを指している必要があります(そのため、関数名を見つけるための`Module構造体があります)。`Moduleオブジェクトにエクスポートされた関数のみが呼び出し可能です。Wasm Workersでは、C/C++関数はエクスポートする必要がなく、任意の関数を送信できます。

Emscripten Worker APIは、以下の場合に使用します。
  • Emscriptenを使用してビルドされていないJSファイルからWorkerを簡単に生成したい場合

  • メインスレッドプログラムとは別の単一のコンパイル済みプログラムをWorkerとして生成したい場合、かつメインスレッドプログラムとWorkerプログラムが共通のコードを共有しない場合

  • SharedArrayBufferの使用や、COOP+COEPヘッダーの設定を必要としない場合

  • postMessage()関数呼び出しを使用して非同期的にWorkerと通信するだけで良い場合

Wasm Workers APIは、以下の場合に使用します。
  • 同じWasmモジュールコンテキストで同期的に計算を行う1つ以上の新しいスレッドを作成したい場合

  • 同じコードベースから複数のWorkerを生成し、WebAssemblyモジュール(オブジェクトコード)とメモリ(アドレス空間)をWorker間で共有することでメモリを節約したい場合

  • アトミックプリミティブとロックを使用してスレッド間の通信を同期的に調整したい場合

  • Webサーバーが、サイトでSharedArrayBuffer機能を有効にするために必要なCOOP+COEPヘッダーで構成されている場合

制限事項

現在、Wasm Workersでは以下のビルドオプションがサポートされていません。

  • -sSINGLE_FILE

  • 動的リンク(-sLINKABLE、-sMAIN_MODULE、-sSIDE_MODULE)

  • -sPROXY_TO_WORKER

  • -sPROXY_TO_PTHREAD

コード例

さまざまなWasm Workers API機能のコード例については、`test/wasm_workers/ディレクトリを参照してください。