Preamble
Rust’s concurrency story is not only ownership—it is also Send and Sync, marker traits the compiler uses to prove whether values may cross thread or task boundaries safely. When Rust and Tokio: The Same Concurrent Workload in Type-Safe Threads’s benchmark introduced shared aggregators, these traits stopped being trivia and became API design.
Send: moving ownership across threads
A type is Send when ownership can transfer to another thread without breaking aliasing rules. Most owned data is Send; Rc is a classic counterexample for naive sharing. If a spawned task closure captures something non-Send, the compiler refuses—good—because you were about to ship a data race.
Sync: shared references across threads
Sync means &T is Send: many threads may hold immutable references concurrently when the type’s interior rules allow. Mutex<T> is Sync when T is Send—the mutex serializes mutation so references stay disciplined.
Aggregators in the benchmark
Global counters tempt Arc<Mutex<Stats>>. That works until contention dominates; then message passing to a single owner task often simplifies invariants and reduces lock thrashing. The “right” choice is measured, not ideological.
Compiler errors as design feedback
When Rust rejects a spawn site, the error can be read as a design review from the typechecker. Languages without these checks still have the races—they just discover them after deploy, with fewer breadcrumbs.
Code: Arc<Mutex<T>> versus a single-owner stats task
Shared mutex (simple, contends under load):
use std::sync::{Arc, Mutex};
// use tokio::sync::mpsc; // in full binary
#[derive(Default)]
struct Stats { done: u64, errors: u64 }
// async fn worker(..., stats: Arc<Mutex<Stats>>) {
// let mut g = stats.lock().unwrap();
// g.done += 1;
// }
Message passing (often less thrash; mirrors a dedicated collector process on the BEAM):
pub struct StatsDelta { pub done: u64, pub errors: u64 }
// async fn stats_task(mut srx: mpsc::Receiver<StatsDelta>) {
// let mut acc = StatsDelta { done: 0, errors: 0 };
// while let Some(d) = srx.recv().await {
// acc.done += d.done;
// acc.errors += d.errors;
// }
// }
Uncomment and wire mpsc channels when you port the JSONL metrics from A Language-Agnostic Concurrent Workload for 2025 Comparisons: one owner task serializes updates without a hot mutex. If profiling shows lock wait is negligible, Arc<Mutex<Stats>> stays the smaller program.
Conclusion
Prefer message passing when it clarifies ownership; use locks when metrics structures are genuinely shared and hot—then profile. Debugging Concurrent Systems: Books and Practices folds in debugging practices and classic texts so incidents close faster than printf loops.