Arc and Mutex
Shared mutable state: TypeScript vs Rust
Section titled “Shared mutable state: TypeScript vs Rust”In JavaScript, Workers do not share memory by default. Each Worker has its own heap. The only way to share mutable state is via SharedArrayBuffer with Atomics, which is low-level, error-prone, and rarely used in application code.
Rust supports shared mutable state across threads through two types that compose together:
Arc<T>— Atomically Reference Counted. A thread-safe smart pointer that lets multiple threads own the same heap-allocated value. When the lastArcclone is dropped, the value is freed.Mutex<T>— Mutual exclusion lock. Wraps a valueTand ensures only one thread can access the inner value at a time.
Together, Arc<Mutex<T>> is the standard Rust idiom for shared mutable state across threads.
Arc — thread-safe reference counting
Section titled “Arc — thread-safe reference counting”Arc is Rust’s answer to “I need multiple owners across threads”. It is similar to Rc<T> (the single-threaded reference counter), but uses atomic operations for the reference count — safe to update from multiple threads simultaneously.
You clone an Arc to give another thread a handle to the same allocation. Cloning is cheap: it only increments an atomic integer.
Mutex — interior mutability with a lock
Section titled “Mutex — interior mutability with a lock”Mutex<T> wraps a value and protects it with a lock. To access the inner T, you call .lock(), which:
- Blocks until no other thread holds the lock.
- Returns a
MutexGuard<T>— a smart pointer that dereferences to&mut T. - Automatically releases the lock when the
MutexGuardis dropped (end of scope).
This is RAII locking — the same pattern Rust uses for memory: the resource is released automatically when the guard goes out of scope.
// TypeScript: no shared mutable state between Workers by default// SharedArrayBuffer + Atomics is the low-level primitiveconst sab = new SharedArrayBuffer(4);const arr = new Int32Array(sab);
// Worker 1Atomics.add(arr, 0, 1); // atomic increment
// Worker 2Atomics.add(arr, 0, 1); // atomic increment
// Main threadconsole.log(Atomics.load(arr, 0)); // 2
// In practice, most TS apps avoid shared memory between Workers// and use message passing (postMessage) instead.use std::sync::{Arc, Mutex};use std::thread;
fn main() { // Arc<Mutex<u32>>: shared, mutable counter across threads let counter = Arc::new(Mutex::new(0u32)); let mut handles = vec![];
for _ in 0..5 { let c = Arc::clone(&counter); // cheap: just increments atomic refcount let h = thread::spawn(move || { let mut val = c.lock().unwrap(); // acquire lock *val += 1; // mutate inner value // MutexGuard 'val' is dropped here — lock is released }); handles.push(h); }
for h in handles { h.join().unwrap(); }
// Only one owner remains — safe to read the final value println!("final counter = {}", *counter.lock().unwrap()); // 5}Collecting results from multiple threads
Section titled “Collecting results from multiple threads”A common pattern is to let each thread push a result into a shared Vec, then read the aggregate after all threads have joined.
// TypeScript: collect results with Promise.allasync function processAll(items: number[]): Promise<number[]> { return Promise.all(items.map(item => Promise.resolve(item * item) // simulate async work ));}use std::sync::{Arc, Mutex};use std::thread;
fn main() { let results: Arc<Mutex<Vec<u32>>> = Arc::new(Mutex::new(vec![])); let items = vec![1u32, 2, 3, 4, 5]; let mut handles = vec![];
for item in items { let r = Arc::clone(&results); let h = thread::spawn(move || { r.lock().unwrap().push(item * item); }); handles.push(h); }
for h in handles { h.join().unwrap(); }
let mut res = results.lock().unwrap().clone(); res.sort(); // sort for deterministic output println!("squares: {:?}", res); // squares: [1, 4, 9, 16, 25]}Try it
Section titled “Try it”use std::sync::{Arc, Mutex};use std::thread;
fn main() { let counter = Arc::new(Mutex::new(0u32)); let mut handles = vec![];
for _ in 0..5 { let c = Arc::clone(&counter); let h = thread::spawn(move || { let mut val = c.lock().unwrap(); *val += 1; }); handles.push(h); }
for h in handles { h.join().unwrap(); }
println!("final counter = {}", *counter.lock().unwrap());}Compiling…