🍣

Webassemblyマルチスレッドクレート:wasm-mtを試す

2022/03/01に公開

動く・・・動くぞ~

関連記事:

Rustが征くシリーズ過去記事

------------------- ↓ 前書はここから ↓-------------------

WebWokerとWebAssemblyの挙動が大分つかめた。
次は動かないモジュールを動かし考察していく。

Webassemblyマルチスレッド用crateを調べる: Rustが征く(7)で紹介した三つのモジュール

これらをなんとか動かして考察していく。
ここではwasm-mtを試す。

コンパイル条件は

設定項目 設定値
toolchain nightly-2021-07-29
build.target wasm32-unknown-unknown
pasm-pack target no-modules

他の2つと異なりビルド条件が緩め。
ただ、導入難易度はダントツに高い。
(動かないよりは遙かにいいか。。。)

ヾ(・ω<)ノ" 三三三● ⅱⅲ コロコロ♪

------------------- ↓ 本題はここから ↓-------------------

プロジェクト作成

wasm-pack new wasm-mt
cd wasm-mt
cargo install cargo-edit wasm-pack wasm-bindgen-cli miniserve
cargo upgrade
cargo add wasm-mt serde_closure wasm_bindgen_futures
cargo add serde --features "derive"
cargo add web-sys --features "console"

ビルドの各種設定

ツールチェインの設定

rustup toolchain add nightly-2021-07-29
rustup component add rust-src --toolchain nightly-2021-07-29
printf '[toolchain]\nchannel = "nightly-2021-07-29"' | tee ./rust-toolchain.toml
rustup toolchain list
  stable-x86_64-unknown-linux-gnu (default)
  nightly-2021-07-29-x86_64-unknown-linux-gnu (override)

実装

ちょっと長いのでアコーディオン化
40億回ループしてconsoleに表示。
同期非同期と別々に実行している。

ソースコード
./src/lib.rs
use wasm_mt::prelude::*;
use wasm_mt::utils::console_ln;

use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;

#[wasm_bindgen]
pub fn app(thread_num: i32) {
    spawn_local(async move {
        let pkg_js = "./pkg/wasm_mt.js";
        let mt = WasmMt::new(pkg_js).and_init().await.unwrap();

        let _ = run_executors(mt, thread_num).await;
    });
}

async fn run_executors(mt: WasmMt, thread_num: i32) -> Result<(), JsValue> {
    // Prepare threads
    let max_num: u64 = 40_0000_0000;
    let mut v: Vec<wasm_mt::Thread> = vec![];
    for i in 0..thread_num {
        let th = mt.thread().and_init().await?;
        th.set_id(&i.to_string());
        v.push(th);
    }

    console_ln!("🔥 serial executor:");
    for th in &v {
        console_ln!("starting a thread");
        let ans = exec!(th, move || {
            for _i in 0..max_num {}
            Ok(JsValue::from(42))
        })
        .await?;
        console_ln!("ans: {:?}", ans);
    }

    console_ln!("🔥 parallel executor:");
    for th in v {
        spawn_local(async move {
            console_ln!("starting a thread");
            let ans = exec!(th, move || {
                for _i in 0..max_num {}
                Ok(JsValue::from(42))
            })
            .await
            .unwrap();
            console_ln!("ans: {:?}", ans);
        });
    }

    Ok(())
}

ビルド

wasm-pack build --release --target no-modules -d ./dist/pkg

フロントエンド側の準備

./dist/index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <script src="./pkg/wasm_mt.js"></script>
  <script src="./main.js"></script>
</head>
<body>
</body>
</html>
./dist/main.js
(async() => {
    const maxWorkers = navigator.hardwareConcurrency || 4;
    wasm_bindgen('./pkg/wasm_mt_bg.wasm').then((wasm) => {
        wasm.app(maxWorkers);
      });    
})()

スレッド数はお好みで

結果

Webからアクセスして動作を見てみる。

miniserve ./dist

(・∀・) 動いてる動いてる

スレッド数を3にして実行してみたが、
直列ではstart - 結果を順に表示、
並列ではstartが3連続、結果が3連続。
40億回ループなんて一瞬で終わってるのが面白い。

------------------- ↓ 後書はここから ↓-------------------

Blob URLってなんだ?

さて、結果は重畳だったのだが、
Network欄を見てみると

blob:http://localhost:3000/~~~ というURLらしきものが並んでる。

(?_?) ナニコレ

調べてみるとblob URLというものがあるらしい。
バイナリファイルをロードして仮想のURLを発行。
URLを利用した各種APIに利用できる・・・らしい。
(あってるかな。)

・・・となると、wasmバイナリをロードし、
指定の数だけBlogURLを発行。
それをスレッドに見立てて動かしてるってことかな。

唯一動作確認はできたのだが、
これはマルチスレッドなのかよくわからない。
複雑な計算をしてじっくり分析が必要かもしれない。

もう少し追ってみると、
どうやらブラウザ対応を意識しているようだ。
WebWorkerは意外とIEも対応してたりするが、
例えばSharedArrayBufferなんかはsafariですら対応していない。

ビルドターゲットno-modulesをどう扱うか

以前、マニアックだから読まなくて良いよ的な記事を書いた。
wasm-packのターゲットをno-moduleにしたときの利用法

実は時系列で話すと、
マルチスレッドのcrateはかなり早い段階で発見して、
動作確認もしていたのだが、
ビルドターゲットとバンドラーをどう組み合わせるかでいろんなパターンを試した。
(ほぼほぼダメだったけど)

no-modulesはHTMLで直接使用することを前提にしているが、
バンドラーを使わず開発など個人レベルでもあり得ないので、
良い落とし所を探していた。

現状ではバンドラーが生成するjsファイルとは別にHTML上でロードして、
グローバル変数として生成される wasm_bindgenというパラメータにwasmファイルを食わせるのが現状可能な方法だ。

一例を挙げると、
ビルド前のソースを main.ts
ビルド後のバンドラーファイルを bundle.js とすると

./src/main.ts
const app = (async () => {
  const maxWorkers = navigator.hardwareConcurrency || 4;
  const path = new URL('/pkg/wasm_mt_bg.wasm', import.meta.url)
  const wasm = await wasm_bindgen(path)
  wasm.app(maxWorkers)
})()

HTML側で両方ロードする

./index.html
<script src='/pkg/wasm_mt.js'></script>
<script defer src='/build/bundle.js'></script>

これについては別記事に書くと思う。

Discussion