Rust on WebAssemblyでマルチスレッド用のMutexを使う
偽物のMutex
Rust + WASM (wasm32-unknown-unknown等)でstd::sync::Mutex
を使うと、表面上コンパイルは通るがその中身はシングルスレッド専用のものとなってしまう。
具体的には、Mutexの二重ロックを試みると単純にpanicするようになっている。(実装) なぜこうなっているのかといえば、そもそもWebAssemblyは基本シングルスレッドで動くものだからで、マルチスレッドを想定する必要もないからということだと思われる。
一方、最近のWASMランタイムではshared memoryがサポートされてきており、これとWebWorker等を組み合わせることで一応マルチスレッドは実現可能となっている。このような場合に上記のようなシングルスレッド用のMutexを生成されても役に立たないので、これをどうにかしてマルチスレッド用のものに切り替えてみる。
WASMとatomicsのあらまし
そもそもWASMでマトモな(spinlockとかでない)Mutexを作れるのかという話だが、実は意外と道具立ては整備されてきている。WASMにはthreadingというproposalがあり、atomicsに関する命令が追加されている(cmpxchgやwait/notifyなど)。ちなみにthreadingという名前ではあるもののスレッド自体を生成できるようになるわけではない。スレッドをどう生成するかというのはWASIで決めようとしているようだがまだ一般利用できるというほど浸透していない。このproposalでカバーしようとしているのは前述のようにweb workerなどで並列処理を実現できた場合にshared memoryをうまく扱えるようにメモリまわりを整備しましょうということだと思われる。(少なくとも自分はそういう使い方がしたかった)
Rustではこのへんはatomicsという名前で定義されていて、 -C target-feature=+atomics
を指定することでatomics関連の機能(core::arch::wasm32内
)を使用できるようになる。
もちろんintrinsicsを駆使していろいろな同期機構を自分で実現してもよいが、実はstd::sync::Mutex
とかも内部的にこれを使えるようになっているみたいなのでせっかくなら使いたい。しかし下に述べる通りこれを使うに当たって落とし穴が存在する。
futexのWASM実装
futex(Mutexの中身)自体はstableではないがWASM用にもすでに実装済みとなっている。futexはプラットフォーム固有の機能(WaitOnAddressやfutex(2)等)の利用が必要なため、プラットフォームごとに固有の実装がされている。WASMの場合はsyscall等用いなくてもthreading proposalで定義されているwait/notifyがあったので十分実装できた模様(つまりWASIはいらない)
WASMのfutexはstd内で #[cfg(target_feature = "atomics")]
でifdefされており、このfeatureを有効にするとそのソース部分が有効になる。重要なのはソースレベルでon/offがされており、target-featureをオンにしたところで既存のバイナリは変化しないという点である。
そもそも、Rust toolchainのtargetに同梱されているstdはすでにビルド済みのバイナリである。(でないと、ツールチェーンをインストールするたびにrustのソースを丸々取得したうえでstdのビルド待ちをする羽目になる)
よって、wasmのfutexを使ったMutexを利用したい場合、そもそもcfgを変更した状態でstdをリビルドしなければならない。これはunstableフラグbuild-stdを指定することでビルドし直すことができる。ただし以下の条件を満たさなければならない。
- unstableフラグを使うためにnightlyツールチェーンを使用
- rustup component add rust-src --toolchain nightly でRustのソースを落としておく
ということでnightlyの使用は必須となる。
ちなみにstdをリビルドせずクレート側でのみ+atomicsを強制的に有効化しようとするとそもそもlldでエラーが出る。
rust-lld: error: --shared-memory is disallowed by std-67b0bca02b914f05.std.cef6b8b2fc89a3fa-cgu.0.rcgu.o because it was not compiled with 'atomics' or 'bulk-memory' features.
rust-lld: error: 'bulk-memory' feature must be used in order to use shared memory
「stdがatomics付きでビルドされていないからshared memoryは有効にできないよ」というエラーが出てしまう。このため、mutexを使う如何にかかわらずそもそも自分でatomicsを使いたい場合でもstdはリビルドしないといけない。
結局どうすりゃいいのさ
まずは.cargo/config.tomlにbuild-stdとtarget-featuresを適切に指定する。
[unstable]
build-std = ["std", "panic_abort"]
[build]
target = "wasm32-unknown-unknown"
rustflags = '-Ctarget-feature=+atomics,+bulk-memory'
保存できたらcargoでビルドを実行する。
cargo +nightly build
とかでビルドする。cargoなしでやる方法はよくわからない。
ちなみにwatに直したらちゃんとwaitやnotifyが使われていた。
(func $_ZN3std3sys4sync5mutex5futex5Mutex4wake17hba217808a9af041aE (type 0) (param i32)
local.get 0
i32.const 1
memory.atomic.notify
drop)
その他注意点
atomicsとbulk-memoryを有効化した影響でshared memoryのリンカフラグが自動で有効になる。これにより、env::memoryにshared memoryがある前提のバイナリとなる。このため、以下のように実行時にshared memoryを与えてやらないと動かない。
const memory = new WebAssembly.Memory({
initial: 128,
maximum: 1024,
shared: true,
});
const wasm = await WebAssembly.instantiate(wasmbin, {
env: { memory },
});
sharedメモリを作成する場合はinitialだけでなくmaximumも設定必須。指定は64KiB単位で行う。上記の例だとMAX64MiBとなる。
また、当たり前だけどatomicsを有効化したらthreading proposalを実装していないプラットフォームでは動かなくなる。このへんはSIMDと同じ雰囲気。一応caniuseで見たらbaseline入りしていたのでそんなに問題はなさそうだが、あらゆるプラットフォームで動かしたい人は注意。
参考文献
build-stdの話が書いてあった。一番役に立った
futexの実装の存在を示唆してくれた。ただ、5年前の情報で一部リンク切れしているので注意。
futexのWASM実装
threading proposal
Discussion
実はWASIでマルチスレッドを実現しようという試みはあまりうまく行かなかったため、組み込みのマルチスレッド機能を付けようと議論されています。まあ、いずれにしてもまだまともに使えない、ということに変わりないのですが...。
ありがとうございます。このようなProposalがあったのですね。実現して面倒な技を使わなくてもマルチスレッドできるようになると嬉しいですね。