Rust's Send & Sync: A Practical Guide to Thread‑Safe Concurrency
Rust’s Send and Sync traits let the compiler enforce thread safety at compile time. Learn how to use Arc and Mutex safely, see a real example, and understand the trade‑offs.
03 Mar 2026, 02:18 UTC

The Problem: Shared State in Multithreaded Rust
When you launch multiple threads, the same memory can be accessed concurrently. If one thread writes while another reads, a data race occurs, leading to undefined behavior. In languages without built‑in safety checks, developers must rely on careful locking or external tools to detect these bugs.
Rust’s Compile‑Time Safety Net
Rust introduces two marker traits, Send and Sync, that let the compiler prove thread safety before the program runs.
- Send: A type is
Sendif its ownership can be safely transferred to another thread. Moving a value across a thread boundary guarantees that only one thread ever owns it, eliminating races on that data. - Sync: A type is
Syncif a shared reference (&T) can be safely accessed by multiple threads concurrently. In practice, this means the data is either immutable or protected by interior mutability primitives.
The compiler automatically implements these traits for a type when all of its fields also implement the required traits. If a type lacks Send or Sync, any attempt to use it across threads will fail at compile time.
Concrete Example: Arc> Across Threads
Below is a minimal program that increments a shared counter from ten concurrent threads. It showcases how Arc (atomic reference counting) and Mutex (exclusive access) work together to satisfy both Send and Sync.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Shared counter wrapped in Arc and Mutex
let counter = Arc::new(Mutex::new(0));
let mut handles = Vec::new();
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
// Lock the mutex for exclusive access
let mut num = counter.lock().expect("Mutex poisoned");
*num += 1;
// Mutex guard released here
});
handles.push(handle);
}
// Wait for all threads to finish
for handle in handles { handle.join().expect("Thread panicked"); }
// Final value should be 10
println!("Result: {}", *counter.lock().expect("Mutex poisoned"));
}
How the compiler enforces safety:
ArcisSendandSyncbecause its inner type is protected by atomic operations.MutexisSendandSyncas long as the data it protects isSendandSync.- The inner
i32is triviallySendandSync, satisfying the compiler.
What Happens If You Pass a Non‑Send Type?
Try compiling the following snippet. The compiler will reject it because Rc (non‑atomic reference counting) is not Send:
use std::rc::Rc;
use std::thread;
fn main() {
let data = Rc::new(5);
thread::spawn(move || {
println!("{}", data);
});
}
Running cargo check yields:
error[E0277]: `Rc<i32>` cannot be sent between threads safely
--> src/main.rs:7:5
|
7 | thread::spawn(move || {
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Rc<i32>` cannot be sent between threads safely
|
= help: the trait `Send` is required for this type
= note: required because it appears within the type `Rc<i32>`
= note: required because it appears within the type `Rc<i32>`
= note: required by `thread::spawn`
Thus the compiler prevents a data race before the program even runs.
Trade‑offs and Limitations
- Runtime overhead:
Arcuses atomic operations for reference counting, which is slightly slower than non‑atomic counters. - Deadlocks: Excessive or nested
Mutexlocks can cause deadlocks if lock order is not carefully managed. - Unsafe trait implementations: You can manually implement
SendorSyncfor a type inside anunsafeblock. Doing so bypasses the compiler’s safety guarantees and can introduce undefined behavior if the type is truly not thread‑safe.
Practical Checklist for Thread‑Safe Rust Code
- Wrap shared data in
Arcif multiple threads need ownership. - Use
Mutex(orRwLock) to protect mutable access. - Prefer immutable sharing where possible;
Arcalone suffices for read‑only data. - Run
cargo checkafter adding new concurrent code to catch trait bound errors. - Avoid
unsafeblocks unless absolutely necessary; if you must, document the rationale and test thoroughly.
Conclusion
Rust’s Send and Sync traits give you a compile‑time guarantee that your concurrent code is free from data races. By combining Arc and Mutex, you can safely share and mutate state across threads. Keep an eye on overhead and deadlock risk, and let the compiler do the heavy lifting for you.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.