Web Assembly Threads の輪郭をなぞる
この記事は Recruit Advent Calendar 2021 の 11 日目の記事です。
はじめに
この記事の多くの部分は以下の記事を参考にさせていただいています。
もっと詳細を知りたい方は是非、下記の記事をご参照ください。
誰に向けた文章か
- Web Assembly をなんとなく知っている
- Web Assembly における Threads とは何か大まかに知りたい
どんな内容か
- Web Assembly Threads の概観
- Rust プロジェクトにおける Web Assembly Threads の紹介
話さないこと
- WASI における Threads
Web Assembly (Wasm) について
Web Assembly (以下 Wasm) は、最近のブラウザで動作する(現在は Wasm Runtime によって他のプラットフォームで動作する場合もある) 高速・高効率でポータブルな新しい種類のコードです。
Wasm のロードマップ
Wasm は未だ発展途上の技術です。
以下のページで、現在の提案とその実装状況を俯瞰することができます。
今回お話する Threads も一部のブラウザではまだ使用することができません。(2021/12/05 時点)
(現在使用しているブラウザが任意の機能を実装しているかを把握したい場合は、GoogleChromeLabs が提供している wasm-feature-detect を利用することをお勧めします。)
しかしながら、TensorFlow.js のように一部のプロジェクトで実践導入されパフォーマンス改善がなされています。
Threads について
前置きが長くなってしまいましたが、ここから Threads について私が調べてきたことを記述していきます。
最初に一番大事なこととして、(少なくともブラウザ上では) Threads は独立した機能ではないということがあげられます。
マルチスレッド的なものの見方を複数のコンポーネントの組み合わせで実現しているというのが実際のところです。
- Web Worker API
- Shared Array Buffer
Wasm で Threads を使用する場合、スレッドの代わりにWorker.js のインスタンスを作成します。作成した各 Worker.js に対しメインスレッドから postMessage() を利用して、WebAssembly.Module とWebAssembly.Memory を共有します。各 Worker.js (スレッド) は共有された WebAssembly.Memory を通して通信を実行します。
(実際の呼び出しイメージは こちら を参照してください)
(ここではメインスレッドが Woker.js を作成していますが、全ての Wasm を Woker.js で動かす例もあります。後述する wasm-bindgen-rayon の example では実際にこのような実装をしています。)
WebAssembly.Memory は通常、ArrayBuffer のラップですが、shared オプションを有効化することで SharedArrayBuffer のラップとなります。Wasm Threads を利用する場合、SharedArrayBuffer を利用する必要があります。WebAssembly.Memory について詳しく知りたい方は公式のリファレンスをご参照ください。
複数のスレッドから共通の配列を参照するとレースコンディションが懸念されますが、この問題は WebAssembly atomics によって解決されます。
WebAssembly atomics では以下の命令を利用してアトミックにメモリにアクセスすることができます。
(このうち atomic.rmw は演算子を利用してより詳細な動作を指定することができます。)
- atomic.load
- atomic.store
- atomic.rmw
また、WebAssembly atomics には notify, wait という 2 つの命令があり、これらを利用してスレッドの動きを中断させたり、再度起動したりすることができます。
Rust で Wasm Threads を使ってみる
Rust で手軽に Wasm Threads を利用する手段として、wasm-bindgen-rayon があります。Rust には元々 Rayon という並行プログラミングに特化したライブラリがあり、それを簡単に Wasm Threads で使えるようにしたのが上記ライブラリです。
さて実装ですが、通常 rayon を利用するように par_iter()
を呼び出すことで、容易に Wasm Threads を利用することができます。
xs
.par_iter()
.map(|x| x * 3)
.sum()
一方で build 設定は通常の開発と違いいくつかの注意点が必要です。
- build は wasm-bindgen もしくは wasm-pack で web-target にする必要がある
- RUSTFLAGS に
+atomic
,+bulk-memory
,+mutable-globals
の指定する必要がある - Wasm 配信サイトがレスポンスヘッダに
Cross-Origin-Embedder-Policy
とCross-Origin-Opener-Policy
を指定する必要がある
1 について
wasm-bindgen-rayon がブラウザに最適化しているため(JS Snippets を内部で利用しているため)この制限が発生します。
また、そもそも Rust 本体が Wasm の移植性を担保するために Wasm Threads をサポートしていないことも理由となっています。
2 について
+atomic
, +bulk-memory
はアトミックにメモリアクセスできる命令を利用するために必要な設定です。 +mutable-globals
はスレッドローカルな値を動的にリンクできるようにするための設定です。いずれの設定も WebAssembly.Memory(SharedArrayBuffer) を利用する上で必須の設定であり、Rust で Wasm Threads を利用する場合には必ず RUSTFLAGS に含める必要があります。
3 について
個人で Wasm Threads を試す場合、一番忘れがちな注意事項です。この設定は SharedArrayBuffer を利用する場合に必要な設定となります。SharedArrayBuffer はSpectre のような脆弱性攻撃の標的となるため、通常はブラウザで利用することができません。Cross-Origin-Embedder-Policy
と Cross-Origin-Opener-Policy
それぞれに特定の値を持たすことで初めて利用することができます。実際に指定する値は以下の通りです。
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy
と Cross-Origin-Opener-Policy
については以下の記事が詳しいので是非ご参照ください。
さいごに
Wasm Threads は発展途上の技術です。そのため、色々な制限や注意事項が多く存在します。
一方で、この技術によって得られるメリットが大きいとも言えます。先述した TensorFlow.js の例や Sqouosh などのサービスに利用されています。今後、高度な計算処理やリッチな画像処理でこの技術が利用されるかもしれません。
この機会に一度 Wasm Threads を触ってみてはいかがでしょうか?