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
.
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 ]
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.
- wasm-bindgen developers
- @alecmocatta for the serde_traitobject crate
- swc-project that facilitates the wasm-mt-test crate
Requirements:
- rustc (nightly)
wasm-pack build
with the--target no-modules
option
Cargo.toml:
wasm-mt = "0.1"
serde = { version = "1.0", features = ["derive"] }
serde_closure = "0.2"
First, create a [WasmMt
] thread builder with [new
][WasmMt::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
][WasmMt::thread] function and initialize it:
let th = mt.thread().and_init().await.unwrap();
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));
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));
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)