#web-worker #worker-thread #multi-threading #thread-pool #async-await #closures

nightly wasm-mt

A multithreading library for Rust and WebAssembly

4 releases

0.1.3 Nov 30, 2022
0.1.2 Jun 29, 2021
0.1.1 Mar 15, 2021
0.1.0 May 25, 2020

#246 in WebAssembly


Used in 4 crates

MIT/Apache

52KB
715 lines

wasm-mt

Docs | GitHub | Crate

crates MIT licensed CI

A multithreading library for Rust and WebAssembly.

wasm-mt helps you create and execute Web Worker based threads. You can program the threads simply using Rust closures and orchestrate them with async/await.

Examples

  • wasm-mt-pool - Thread pool library based on wasm-mt. [ crate | source ]

You can run all the following apps in browser!

  • exec - How to use wasm_mt. [ live | source ]
  • fib - Computing a Fibonacci sequence with nested threads. [ live | source ]
  • executors - Minimal serial/parallel executors using wasm_mt. [ live | source ]
  • parallel - Julia set benchmark of serial/parallel executors. [ live | source ]
  • arraybuffers - Demo of using WasmMt::new_with_arraybuffers(). [ live | source ]

Background and implementation

The preceding seminal work entitled "Multithreading Rust and Wasm" by @alexcrichton centers on Web Workers, shared memory, and the WebAssembly threads proposal. Shared memory is built on top of SharedArrayBuffer whose availability across major browsers has been somewhat limited. Also, the rust-wasm thread implementation work, along with the threads proposal, seems still in progress.

On the contrary, Web Worker based multithreading in JavaScript has been well supported for a long time. After experimenting, we have come up to a Rust ergonomic multithreading solution that does not require SharedArrayBuffer. It just works across all major browsers today and we named it wasm-mt.

Internally, we use the postMessage() Web Worker API (through bindings provided by wasm-bindgen) to initialize spawned threads. And, importantly, we keep using postMessage() for dynamically sending Rust closures (serialized by serde_traitobject) to the spawned threads. By doing so, the parent thread can await the results of the closures executed in the spawned thread. We have found that this approach is highly flexible for extension, too. For example, it is straightforward to augment WasmMt::Thread to support more customized inter-thread communication patterns.

Note, however, that wasm-mt has some remarkable limitations compared to the ongoing shared memory based multithreading work led by wasm-bindgen. wasm-mt is not efficient in that it does not include support of the standard thread primitive operations:

  • shared memory based message passing and mutexes,
  • atomic instructions and efficient memory handling per the threads proposal.

Thanks

Getting started

Requirements:

Cargo.toml:

wasm-mt = "0.1"
serde = { version = "1.0", features = ["derive"] }
serde_closure = "0.3"

Creating a thread

First, create a WasmMt thread builder with new and initialize it:

use wasm_mt::prelude::*;

let pkg_js = "./pkg/exec.js"; // path to `wasm-bindgen`'s JS binding
let mt = WasmMt::new(pkg_js).and_init().await.unwrap();

Then, create a [wasm_mt::Thread][Thread] with the thread function and initialize it:

let th = mt.thread().and_init().await.unwrap();

Executing a thread

Using the exec! macro, you can execute a closure in the thread and await the result:

// fn add(a: i32, b: i32) -> i32 { a + b }

let a = 1;
let b = 2;
let ans = exec!(th, move || {
    let c = add(a, b);

    Ok(JsValue::from(c))
}).await?;
assert_eq!(ans, JsValue::from(3));

You can also execute an async closure with exec!:

// use wasm_mt::utils::sleep;
// async fn sub(a: i32, b: i32) -> i32 {
//    sleep(1000).await;
//    a - b
// }

let a = 1;
let b = 2;
let ans = exec!(th, async move || {
    let c = sub(a, b).await;

    Ok(JsValue::from(c))
}).await?;
assert_eq!(ans, JsValue::from(-1));

Executing JavaScript in a thread

Using the exec_js! macro, you can execute JavaScript within a thread:

let ans = exec_js!(th, "
    const add = (a, b) => a + b;
    return add(1, 2);
").await?;
assert_eq!(ans, JsValue::from(3));

Similarly, use exec_js_async! for running asynchronous JavaScript:

let ans = exec_js_async!(th, "
    const sub = (a, b) => new Promise(resolve => {
        setTimeout(() => resolve(a - b), 1000);
    });
    return await sub(1, 2);
").await?;
assert_eq!(ans, JsValue::from(-1));

Making executors

By using [wasm_mt:Thread][Thread], you can easily create custom executors. One such example is the wasm-mt-pool crate. It provides a thread pool that is based on the work stealing scheduling strategy.

Here, for simplicity, we show the implementation of much more straightforward executors: a serial executor and a parallel executor.

First, prepare a Vec<wasm_mt::Thread> containing initialized threads:

let mut v: Vec<wasm_mt::Thread> = vec![];
for i in 0..4 {
    let th = mt.thread().and_init().await?;
    v.push(th);
}

Then, here's the executors in action. Note, in the latter case, we are using wasm_bindgen_futures::spawn_local to dispatch the threads in parallel.

console_ln!("🚀 serial executor:");
for th in &v {
    console_ln!("starting a thread");
    let ans = exec!(th, move || 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 || Ok(JsValue::from(42))).await.unwrap();
        console_ln!("ans: {:?}", ans);
    });
}

Observe the starting/ending timing of each thread in the developer console:

🚀 serial executor:
starting a thread
ans: JsValue(42)
starting a thread
ans: JsValue(42)
starting a thread
ans: JsValue(42)
starting a thread
ans: JsValue(42)
🚀 parallel executor:
(4) starting a thread
(4) ans: JsValue(42)

Dependencies

~9–12MB
~223K SLoC