JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Send, Sync, and Interior Mutability

How does Rust let safe code share data across threads without a single data race? Two marker traits, Send and Sync, encode thread-safety into the type system, and interior mutability shows how the borrow rules can bend at runtime without ever breaking.

The rule we are about to bend

Everything in this rung so far has rested on one iron law from the first guide: at any moment a value may have many shared readers OR exactly one writer, never both at once. That is aliasing xor mutability — the borrow checker enforces it at compile time, and it is precisely why safe Rust has no data races. A data race is the situation where two threads touch the same memory at the same time, at least one is writing, and nothing orders them. If no value can ever be aliased and mutable simultaneously, that situation is unreachable by construction. The whole story of this guide is: how do we keep that guarantee even when we genuinely need shared, mutable, multi-threaded state?

First, a sharp distinction the whole guide hangs on. Recall from the synchronization rung that concurrency is not parallelism and a shared mutable state is the root of nearly every threading bug. In C, sharing across threads is the default and safety is the thing you must add by hand — every shared write needs a mutex you remembered to take, and forgetting is undefined behavior. Rust flips this: sharing across threads is not the default. The type system refuses to let a value cross a thread boundary unless that value's type has earned the right. The traits that grant that right are called Send and Sync.

Send and Sync: thread-safety as two marker traits

Send and Sync are marker traits — they have no methods at all. A trait with no methods carries no behavior; it only attaches a fact to a type, a fact the compiler can check and reason about. The two facts are almost identical but worth separating carefully. A type is Send if it is safe to move a value of that type to another thread — to hand over sole ownership across the boundary. A type is Sync if it is safe for several threads to hold shared references to one value at once. The crisp relationship: a type T is Sync exactly when the reference type &T is Send. "Sharing &T across threads" and "sending &T to another thread" are the same act.

Here is the quietly brilliant part: you almost never implement these by hand. They are auto traits. The compiler derives Send and Sync automatically for a struct if and only if every one of its fields is Send (or Sync). The property composes up from the leaves. Plain data — an i32, a String, a Vec of Send things — is Send and Sync, so it threads freely. The non-Send types are the few that genuinely carry cross-thread hazard, and the type system propagates that hazard outward: put one non-Send field in your struct and the whole struct loses Send, automatically, with no annotation from you.

Stand back and notice what just happened. "Is this safe to share across threads?" — historically the single hardest question in systems programming, the one you answered with code review, intuition, and prayer — has become a fact the compiler computes about your type, mechanically, from the ground up. The borrow checker from guide one stops two threads aliasing-and-mutating within the language; Send and Sync extend that exact reasoning across thread boundaries. They are the bridge between the borrow rules and real concurrency.

Interior mutability: bending the borrow rule at runtime

Now the second half. Sometimes the compile-time rule is too strict for a correct program. You hold a shared reference `&T` — which normally forbids mutation — yet you genuinely need to mutate the thing behind it, and you can prove to a human that no two mutations actually overlap. The classic case is exactly Rc: every co-owner holds a shared reference to the count, yet cloning must increment it. The pattern that resolves this is interior mutability: a type that lets you mutate through a `&T` by moving the aliasing-xor-mutability check from compile time to runtime.

The rule itself never changes — at most one writer at a time still holds. What changes is when and how it is checked. Three tools form a ladder, each paying a little more for a little more sharing. `Cell<T>` is the cheapest: it forbids handing out any inner reference, so it only lets you get and set whole values by copy; with no references possible, no aliasing check is even needed, and it costs nothing at runtime. `RefCell<T>` is the workhorse: it hands out real borrows but keeps a tiny borrow counter inside, checked at runtime. Borrow it shared and the count goes up; ask for a mutable borrow while a shared one is live and it does not silently corrupt — it panics. The check moved from the compiler to a runtime flag, so violating aliasing-xor-mutability becomes a loud, deterministic crash instead of undefined behavior.

use std::cell::RefCell;
let c = RefCell::new(5);
let a = c.borrow();        // shared borrow  -> runtime count: 1 reader
let b = c.borrow_mut();    // wants a writer while a reader is live
// PANIC at runtime: 'already borrowed: BorrowMutError'
// the rule held -- it was just checked now, not by the compiler

// Cell, by contrast, has no references to count, so no panic is possible:
use std::cell::Cell;
let n = Cell::new(0u32);
n.set(n.get() + 1);        // get a copy, set a copy -- always fine
RefCell moves the borrow check to runtime: the rule is unchanged, the timing is not.

Be honest about the trade you just made. RefCell does not make your program safer than the borrow checker — it makes it more permissive, at the cost of turning a compile error into a possible runtime panic. You have taken on the duty the compiler used to carry. Use it when the borrow pattern is correct but too dynamic for the checker to follow (a graph, a cache, a parent-child structure), not as a reflex to silence borrow errors. A panicking RefCell is a bug you shipped, just one that fails loudly instead of corrupting memory like C would.

Crossing threads: Mutex is RefCell for the multi-threaded world

Cell and RefCell give interior mutability on one thread; both are deliberately not Sync, so the compiler stops you sharing them across threads — their runtime borrow flag is itself not atomic and would race. The multi-threaded counterpart of RefCell is the Mutex you met in the synchronization rung, now wearing Rust's clothes. Where C's `pthread_mutex_lock()` guards a region of code and trusts you to only touch the data while holding the lock, Rust's `Mutex<T>` contains the data. The only way to reach the inner value is to call `.lock()`, which blocks until the lock is yours and then hands back a guard that derefs to the data.

This single design choice closes the most common concurrency bug in C outright: you literally cannot touch the protected data without holding the lock, because the data is only reachable through the guard, and the guard only exists after a successful lock. And when the guard goes out of scope, its Drop implementation releases the lock automatically — the RAII pattern from earlier in this rung, now unlocking a mutex instead of freeing memory. There is no `unlock()` to forget, no early-return path that skips it, no lock leaked on a panic. The lock is tied to a scope exactly the way a heap allocation was tied to a unique_ptr.

To share that Mutex among threads you wrap it in Arc — the atomic-counted owner — giving the idiomatic `Arc<Mutex<T>>`: Arc lets several threads co-own the same Mutex (Arc is Send and Sync), and Mutex makes the shared mutation safe. Read the type and the safety argument falls out: Mutex<T> is Sync as long as T is Send, because the lock guarantees only one thread is inside at a time, so even a merely-Send T is safe to mutate through it. The whole zoo — Cell, RefCell, Mutex, Rc, Arc — is one idea in escalating strength: regain shared mutability you can prove correct, and let the type system, a runtime flag, or a lock carry the proof.

Putting it together, and where unsafe comes in

Let us trace the safe path from one shared counter all the way down. Suppose four threads each need to bump a shared integer. You write `let n = Arc::new(Mutex::new(0));`, clone the Arc once per thread, and inside each thread do `*n.lock().unwrap() += 1`. Walk the guarantees: Arc being Send lets each clone cross the spawn boundary; Arc being Sync lets all four co-own; Mutex serializes the mutations; the guard's Drop unlocks on the way out. Four moving parts, and the compiler checked every one before the program ever ran.

  1. Ask first: does this even need sharing across threads? If one thread owns the data, no Arc, no Mutex, no atomics — just move it in and keep the simplest thing.
  2. If you need shared ownership across threads, reach for Arc (Send + Sync); single-threaded shared ownership stays the cheaper Rc.
  3. If the shared thing must also be mutated, wrap the data in Mutex (or RwLock); on one thread the lighter RefCell or Cell does the same job.
  4. Read the resulting type aloud as the spec — Arc<Mutex<T>> literally says "shared, locked, mutable" — and trust the compiler to reject the combinations that are not thread-safe.

One honest loose end ties this guide to the next. Send, Sync, Cell, RefCell, Mutex, Arc — every one of them is implemented, deep down, with unsafe code. Auto-derivation of Send and Sync can be wrong for a type the compiler cannot see into (one built from raw pointers), so the author of such a type must write `unsafe impl Send for MyType {}` — a hand-signed promise to the compiler: "I have checked the invariant the auto-rule could not." Arc's atomic counter, RefCell's flag-checked borrows, the Mutex guard — all sit on a tiny core of unsafe that a human audited once so that the millions of lines built on top can stay entirely safe.

That is the real shape of the bargain, and it is fair to state it plainly. Safe Rust does not abolish low-level danger; it concentrates it. The hazardous reasoning that in C is smeared across every line of a threaded program gets gathered into a few audited, unsafe building blocks, wrapped in safe types whose signatures encode exactly when they may be used. The next guide opens that core directly: what unsafe really permits, the contract you sign when you write it, and how this same machinery reaches across the FFI boundary back into the C world this rung began in.