Pthreads サポート

実装され有効化されているブラウザでは、SharedArrayBuffer を Cross Origin Opener Policy (COOP) および Cross Origin Embedder Policy (COEP) ヘッダーの背後にゲートしています。これらのヘッダーが正しく設定されていない限り、Pthreads コードはデプロイ環境では動作しません。詳細については、こちらをクリックしてください。

Emscripten は、ブラウザで SharedArrayBuffer を使用したマルチスレッドをサポートしています。この API は、メインスレッドと Web ワーカー間でのメモリ共有、および同期のためのアトミック操作を可能にし、これにより Emscripten は Pthreads (POSIX スレッド) API のサポートを実装できます。このサポートは Emscripten で安定していると見なされています。

pthreads を有効にしてコンパイルする

デフォルトでは、pthreads のサポートは有効になっていません。pthreads のコード生成を有効にするには、次のコマンドラインフラグがあります。

  • コンパイル時、および最終的な出力 .js ファイルを生成するためのリンク時に、コンパイラフラグ -pthread を渡します。

  • オプションで、リンカフラグ -sPTHREAD_POOL_SIZE=<expression> を渡して、アプリケーションの main() が呼び出される前のページ preRun 時に、事前に定義された Web ワーカーのプールを設定するように指定します。ワーカーがまだ存在しない場合、特定のことのために次のブラウザイベントのイテレーションを待つ必要がある可能性があるため、これは重要です。以下を参照してください。<expression> は、固定数のスレッドを表す 8 のような整数や、CPU コア数と同じ数のスレッドを作成する navigator.hardwareConcurrency など、有効な JavaScript 式にすることができます。

その他の変更は必要ありません。C/C++ コードでは、プリプロセッサチェック #ifdef __EMSCRIPTEN_PTHREADS__ を使用して、Emscripten が現在 pthreads をターゲットにしているかどうかを検出できます。

利用可能な場合はマルチスレッドを活用し、そうでない場合はシングルスレッドにフォールバックできる1つのバイナリをビルドすることはできません。可能な最善策は、スレッドありとなしの2つの個別のビルドを作成し、実行時にどちらかを選択することです。

追加のフラグ

  • -sPROXY_TO_PTHREAD: このモードでは、元の main() が新しいものに置き換えられ、pthread を作成し、元の main() をその上で実行します。結果として、アプリケーションの main() はブラウザのメイン (UI) スレッドからオフになり、これは応答性に優れています。ブラウザのメインスレッドは、イベントの処理やレンダリングなど、何かがプロキシされるときにコードを実行します。メインスレッドは、pthreads を作成するなど、同期的に依存できるようにします。

Emscripten には --proxy-to-worker リンカフラグ がありますが、これは似ていますが無関係です。そのフラグは、pthreads や SharedArrayBuffer を使用する代わりに、プレーンな Web Worker を使用してメインプログラムを実行します (そして、postMessage を使用してメッセージをやり取りします)。

プロキシ

Web では、DOM の操作など、特定の操作がメインブラウザスレッドからのみ行われるように許可されています。結果として、バックグラウンドスレッドで呼び出された場合、さまざまな操作がメインブラウザスレッドにプロキシされます。詳細とそれまでの回避方法については、バグ 3495 を参照してください。どの操作がプロキシされるかを確認するには、JS ライブラリ (src/library_*) で関数の実装を探し、__proxy: 'sync' または __proxy: 'async' で注釈が付いているかどうかを確認できます。ただし、ブラウザ自体が特定のこと (一部の GL 操作など) をプロキシするため、ここで安全を確保する一般的な方法はないことに注意してください (メインブラウザスレッドをブロックしないことを除いて)。

さらに、Emscripten は現在、ファイル I/O がメインアプリケーションスレッドでのみ発生するという単純なモデルを持っています (メモリを共有できない JS プラグインファイルシステムをサポートしているため)。これもプロキシされる操作のセットです。

プロキシは特定の場合に問題を引き起こす可能性があります。以下のブロッキングに関するセクションを参照してください。

メインブラウザスレッドでのブロッキング

ほとんどの場合、「メインブラウザスレッド」は「メインアプリケーションスレッド」と同じであることに注意してください。メインブラウザスレッドは、Web ページが JavaScript の実行を開始する場所であり、JavaScript が DOM にアクセスできる場所です (ページは Web Worker を作成することもできますが、その場合、メインスレッド上にはありません)。メインアプリケーションスレッドは、アプリケーションを開始したスレッドです (Emscripten によって出力されたメイン JS ファイルをロードすることによって)。それが通常の HTML ページであるため、メインブラウザスレッドで起動した場合、2つは同一です。ただし、ワーカーでマルチスレッドアプリケーションを起動することもできます。その場合、メインアプリケーションスレッドはそのワーカーであり、メインブラウザスレッドへのアクセスはありません。

アトミックス用の Web API では、メインスレッドでのブロッキングは許可されていません (具体的には、Atomics.wait はそこで機能しません)。このようなブロッキングは、pthread_join や、内部で futex wait を使用する usleep()emscripten_futex_wait()pthread_mutex_lock() などの API で必要です。それらを機能させるために、メインブラウザスレッドでビジーウェイトを使用しますが、これによりブラウザータブが応答しなくなり、電力も無駄になります。(pthread の場合、Web Worker で実行されるため、ビジーウェイトする必要がないため、これは問題ではありません。)

一般的に、メインブラウザスレッドでのビジーウェイトは、わずかに競合するミューテックスを待つ場合など、上記の欠点があるにもかかわらず機能します。ただし、pthread_joinpthread_cond_wait のようなものは、多くの場合、長時間ブロックすることを目的としており、それがメインブラウザスレッドで発生し、他のスレッドが応答を期待している間は、驚くべきデッドロックを引き起こす可能性があります。これは、プロキシが原因で発生する可能性があります。前のセクションを参照してください。ワーカーがプロキシを試みている間にメインスレッドがブロックすると、デッドロックが発生する可能性があります。

結論として、Web では、メインブラウザスレッドが他のものを待つのは良くありません。したがって、デフォルトでは、pthread_join および pthread_cond_wait がメインブラウザスレッドで発生した場合、Emscripten は警告を発し、ALLOW_BLOCKING_ON_MAIN_THREAD がゼロの場合 (そのメッセージはこちらを指します)、エラーがスローされます。

これらの問題を回避するには、前述したように main() 関数を pthread に移動する PROXY_TO_PTHREAD を使用できます。これにより、メインブラウザスレッドはプロキシされたイベントの受信のみに集中できるようになります。これは一般的に推奨されますが、アプリケーションが main() がメインブラウザスレッド上にあると想定していた場合、多少の移植作業が必要になる可能性があります。

別のオプションは、ブロッキング呼び出しをノンブロッキング呼び出しに置き換えることです。たとえば、pthread_joinpthread_tryjoin_np に置き換えることができます。これには、emscripten_set_main_loop() または ASYNCIFY を使用して、アプリケーションを非同期イベントを使用するようにリファクタリングする必要がある場合があります。

特別な考慮事項

pthreads API の Emscripten 実装は POSIX 標準に厳密に従う必要がありますが、動作の違いがいくつか存在します。

  • pthread_create() が呼び出されたとき、新しい Web Worker を作成する必要がある場合、メインイベントループに戻る必要があります。つまり、pthread_create を呼び出した後、Worker が実行を開始することを期待してコードを同期的に実行し続けることはできません。Worker はイベントループに戻った後にのみ実行されます。これは POSIX の動作に違反し、スレッドを作成してすぐに join したり、メモリ書き込みなどの効果を同期的に待機したりする一般的なコードを壊す可能性があります。これにはいくつかの解決策があります。

    1. メインイベントループに戻る(例えば、emscripten_set_main_loop または Asyncify を使用)。

    2. リンカーフラグ -sPTHREAD_POOL_SIZE=<expression> を使用する。プールを使用すると、main が呼び出される前に Web Worker が作成されるため、pthread_create が呼び出されたときに使用できます。

    3. リンカーフラグ -sPROXY_TO_PTHREAD を使用する。これにより、main() が Worker 上で実行されます。その場合、pthread_create はメインブラウザースレッドにプロキシされ、必要に応じてメインイベントループに戻ることができます。

  • Emscripten の実装は、POSIX シグナルをサポートしていません。これは、Web Worker にシグナルを送信してその実行をプリエンプトすることが不可能であるためです。唯一の例外は pthread_kill() で、これは通常どおり実行中のスレッドを強制終了するために使用できます。

  • Emscripten の実装は、fork() および join() を介したマルチプロセッシングもサポートしていません。

  • Web セキュリティ上の理由から、Firefox Nightly で実行する場合に生成できるスレッド数には固定制限(デフォルトで 20)があります。#1052398。制限を調整するには、about:config に移動し、設定 “dom.workers.maxPerDomain” の値を変更します。

  • Emscripten が利用するアップストリームの musl ライブラリがそれらをサポートしていないか、オプションとしてマークされており、準拠した実装がそれらをサポートする必要がないため、pthreads 仕様の一部の機能はサポートされていません。Emscripten でサポートされていない機能には、スレッドの優先度設定や、pthread_rwlock_unlock() がスレッド優先度順に実行されないことなどがあります。関数 pthread_mutexattr_set/getprotocol()、pthread_mutexattr_set/getprioceiling()、および pthread_attr_set/getscope() は no-op です。

  • 移植時に特に注意すべき点の1つは、既存のコードベースで pthread_create() および pthread_cleanup_push() へのコールバック関数ポインターが void* 引数を省略している場合があることです。これは厳密に言えば C/C++ では未定義の動作ですが、いくつかの x86 呼び出し規約では機能します。これを Emscripten で行うと、コンパイラーの警告が表示され、署名が正しくない関数ポインターを呼び出そうとするとランタイムで中止される可能性があるため、そのようなエラーがある場合は、スレッドコールバック関数の署名を確認することをお勧めします。

  • 関数 emscripten_num_logical_cores() は、共有メモリがサポートされていない場合でも、常に navigator.hardwareConcurrency の値、つまりシステムの論理コア数を返します。これは、emscripten_num_logical_cores() が 1 より大きい値を返す一方で、emscripten_has_threading_support() が false を返す可能性があることを意味します。emscripten_has_threading_support() の戻り値は、ブラウザーが共有メモリのサポートを利用できるかどうかを示します。

  • Pthreads + メモリ拡張(ALLOW_MEMORY_GROWTH)は特にトリッキーです。Wasm 設計上の問題 #1271 を参照してください。これにより、現在、Wasm メモリにアクセスする JS が遅くなります。ただし、これは JS が大量のメモリ読み取りおよび書き込みを行う場合にのみ顕著になる可能性があります(Wasm はフルスピードで実行されるため、ワークを移動することでこれを修正できます)。また、JS が HEAP* ビューを更新する必要があることに注意する必要もあります。 --js-library などで埋め込まれた JS コードは、HEAP* が使用されている場合に GROWABLE_HEAP_* ヘルパー関数を使用するように自動的に変換されますが、Module.HEAP* を直接使用する外部コードでは、ビューがメモリよりも小さいため問題が発生する可能性があります。

アロケーターのパフォーマンス

Emscripten のデフォルトのシステムアロケーターである dlmalloc は、シングルスレッドプログラムでは非常に効率的ですが、グローバルロックが 1 つであるため、malloc で競合が発生するとオーバーヘッドが発生する可能性があります。代わりに -sMALLOC=mimalloc を使用して mimalloc を使用できます。これは、マルチスレッドのパフォーマンスに合わせて調整された、より洗練されたアロケーターです。mimalloc は各スレッドに個別の割り当てコンテキストを持つため、malloc/free の競合下でパフォーマンスが大幅に向上します。

mimallocdlmalloc よりもコードサイズが大きく、実行時にもより多くのメモリを使用するため(INITIAL_MEMORY をより高い値に調整する必要がある場合があります)、ここにはトレードオフがあることに注意してください。

コードとテストの実行

pthreads のサポートを有効にしてコンパイルされたコードは、SharedArrayBuffer 仕様がまだ標準化前の実験的研究段階にあるため、現在 Firefox Nightly チャネルでのみ動作します。Emscripten での pthreads API 実装の動作を検証するために使用できる 2 つのテストスイートがあります。

  • Emscripten ユニットテストスイートには、「browser」スイートにいくつかの pthreads 固有のテストが含まれています。browser.test_pthread_* という名前のテストを実行してください。

  • Open POSIX Test Suite の Emscripten に特化したバージョンは、juj/posixtestsuite GitHub リポジトリで入手できます。このスイートには、pthreads 適合性に関する約 300 のテストが含まれています。このスイートを実行するには、まず、設定 dom.workers.maxPerDomain を少なくとも 50 に増やす必要があります。

問題が発生した場合は、まずこれらを確認してください。バグは通常どおり Emscripten バグトラッカーに報告できます。