| |

Rust 64 🦀 Sync and Send Traits for Multi-Threaded Safety

⏱️ Estimated read: 17 min | |

Rust’s concurrency story rests on two marker traits that have no methods, no runtime cost, and no counterpart in most other languages: Send and Sync. Everything else in this chapter series—threads, channels, mutexes—ultimately depends on the compiler’s ability to reason about which types can cross thread boundaries and which cannot. These two traits are how that reasoning is encoded.

The design is unusual in its minimalism. Send and Sync are auto traits: the compiler implements them automatically for any type composed of parts that already satisfy them . You almost never write impl Send for MyType in ordinary application code. Instead, you write structs and enums from Send and Sync components, and the compiler derives the properties for you. When the derivation fails—when you try to move an Rc<T> into a spawned thread, for example—the error message tells you exactly which type broke the chain and why .

What makes these traits powerful is what they prevent. Without Send and Sync, the Rust type system would still enforce memory safety within a single thread, but concurrent code would be a free-for-all. You could clone an Rc, send it to another thread, and corrupt the reference count silently. You could share a RefCell and trigger undefined behavior through unsynchronized borrow flags. Send and Sync close those doors at compile time.

This chapter covers what each trait means, how the compiler derives them, which standard-library types are and are not thread-safe, why the &T is Send if T is Sync relationship matters, and when manual unsafe impl is justified. By the end you will know not just which types to use but why the compiler rejects the ones it does.

Key point: Send means ownership of a value can be transferred to another thread; Sync means shared references (&T) to a value can be used from multiple threads safely. The precise relationship is: T: Sync if and only if &T: Send.


Why Send and Sync exist

The problem of unsynchronized sharing. Rust’s ownership system prevents data races within a single thread by enforcing that mutable references are exclusive and that values have exactly one owner at a time. But threads break this model: two threads can own clones of the same Rc<T>, both can attempt to decrement the reference count simultaneously, and the non-atomic count can be corrupted . Send and Sync extend the ownership discipline to the threaded case.

Auto traits and structural derivation. Most traits in Rust require explicit impl blocks. Send and Sync are different: they are auto traits, meaning the compiler automatically implements them for any type whose components all implement them . A struct containing an i32 and a String is automatically Send and Sync because both fields are. A struct containing an Rc<T> is automatically neither, because Rc is neither. This structural derivation makes the common case trivial and the exceptional case loud.

The &T relationship. Sync is defined in terms of Send: a type T is Sync if and only if &T is Send . This definition is precise and worth internalizing. It means that if you can send an immutable reference to another thread safely, the type is Sync. Mutex<T> is Sync because &Mutex<T> can be sent safely—the mutex itself handles synchronization internally .

UnsafeCell and interior mutability. The core of the Sync exclusion is UnsafeCell, the only type for which Rust does not guarantee no aliasing mutation. Cell<T>, RefCell<T>, and Rc<T> all contain UnsafeCell internally, which is why none of them are Sync . They provide interior mutability without synchronization, which is safe single-threaded but undefined behavior when shared across threads.

The unsafe contract. Send and Sync are unsafe traits. Implementing them manually is a promise to the compiler and to all other unsafe code that your type upholds the thread-safety invariant . The compiler cannot verify this promise; it trusts you. This is why manual implementations are rare and why incorrect ones can cause undefined behavior.

The separation of moving and sharing. Send and Sync are related but not interchangeable. Send is about moving ownership: can this value be transferred to another thread? Sync is about sharing references: can multiple threads hold &T simultaneously? A type can be Send but not Sync (e.g., a type with interior mutability that is safe to move but not to share), and in theory can be Sync but not Send (though this is rare in practice) .


a. Send: Transferring ownership across threads

Send marks a type whose values can be moved from one thread to another. The standard library definition is terse: “Types that can be transferred across thread boundaries” .

The key examples from the standard library illustrate the boundary. Primitive types like i32, f64, bool, and char are Send. Compound types built entirely from Send components are automatically Send . Most standard library collections—Vec<T>, HashMap<K, V>, Box<T>—are Send when their contents are .

The canonical exception is Rc<T>. It is not Send because its reference count is a plain non-atomic integer. If two threads cloned an Rc pointing to the same allocation, they could interleave their reference-count updates and corrupt the count, potentially freeing the allocation while the other thread still holds a reference . The compiler rejects any attempt to send an Rc across a thread boundary at compile time.

use std::rc::Rc;
use std::thread;

fn main() {
    let shared = Rc::new(5);
    let cloned = Rc::clone(&shared);

    // This will not compile: Rc<i32> is not Send
    thread::spawn(move || {
        println!("{}", cloned);
    });
}

The error message is specific: “Rc<i32> cannot be sent between threads safely.” The fix is Arc<T>, the atomic reference-counted pointer, which is Send when T is Send and Sync .

The Send bound appears throughout the standard library’s threading API. thread::spawn requires its closure to be Send + 'static, because the closure and its captures move into the new thread . The same bound appears on Sender<T> in mpsc, on JoinHandle<T>, and on most async runtime APIs.


b. Sync: Sharing references across threads

Sync is defined in terms of Send: a type T is Sync if and only if &T is Send . In plain terms, Sync means it is safe for multiple threads to hold immutable references to the same value simultaneously.

The structural derivation works the same way as Send: if all components of a type are Sync, the type is Sync . Primitive types are Sync. Box<T>, Vec<T>, and most collections are Sync when their contents are .

The interesting cases are the types with interior mutability. Cell<T> and RefCell<T> are not Sync because they allow mutation through a shared reference without synchronization . If thread A holds &RefCell<T> and calls borrow_mut(), while thread B holds another &RefCell<T> and calls borrow(), the runtime borrow flag—a plain Cell<isize>—can be corrupted. The compiler rejects this by making RefCell<T> not Sync.

Mutex<T> and RwLock<T> are Sync. They provide interior mutability but serialize access through locks, so shared references are safe . This is why Arc<Mutex<T>> is the canonical pattern for shared mutable state: Arc provides shared ownership (Send + Sync when T is), and Mutex provides synchronized interior mutability .

The &mut T case is subtle and worth noting. A mutable reference &mut T is Sync if T is Sync, even though it seems like it should allow unsynchronized mutation . The resolution is that a &mut T behind a shared reference (i.e., &&mut T) becomes read-only; you cannot mutate through it. So there is no data race risk.

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0u32));

    let handles: Vec<_> = (0..10)
        .map(|_| {
            let c = Arc::clone(&counter);
            thread::spawn(move || {
                *c.lock().unwrap() += 1;
            })
        })
        .collect();

    for h in handles {
        h.join().unwrap();
    }

    println!("{}", *counter.lock().unwrap());
}

Here Arc<Mutex<u32>> is both Send and Sync. Each thread receives a clone of the Arc, and the Mutex serializes the increments. The compiler accepts this because both Arc and Mutex satisfy the bounds.


c. The relationship between the traits and common patterns

The precise relationship between Send and Sync can be summarized in four implications from the standard library :

  • &T is Send if and only if T is Sync
  • &mut T is Send if and only if T is Send
  • &T is Sync if and only if T is Sync
  • &mut T is Sync if and only if T is Sync

The Arc<T> implementation encodes the practical consequence of these rules. Arc<T> is Send if T: Send + Sync. It is Sync if T: Send + Sync . The requirement for both bounds is deliberate: Arc shares ownership, so if T were Send but not Sync, multiple threads could hold references to a value that is not safe to share, leading to data races. The Send bound on Arc<T>: Sync ensures that the value itself can move between threads when the last reference does.

Mutex<T> is Sync if T: Send . The Send bound on T is required because Mutex hands out &mut T access under the lock, and that mutable access could be used to move T out or replace it with a non-Send value. The mutex does not require T: Sync because it serializes all access; no two threads can touch the data simultaneously.

Channels follow the same pattern. Sender<T> and Receiver<T> are Send when T: Send. The receiver is not Sync (you cannot share a receiver across threads), which aligns with the single-consumer design .


Complete Example Session

The following session builds a series of functions that demonstrate Send and Sync in practice, including type-level assertions, the Rc failure, the Arc fix, and the structural derivation of both traits.

// ============================================
// PART 1: IMPORTS AND TYPE ASSERTION HELPERS
// ============================================
use std::rc::Rc;
use std::cell::RefCell;
use std::sync::{Arc, Mutex, RwLock};
use std::thread;

// Compile-time assertions that a type is Send/Sync
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}

// ============================================
// PART 2: PRIMITIVES ARE SEND AND SYNC
// ============================================

fn primitives_are_send_sync() {
    assert_send::<i32>();
    assert_send::<f64>();
    assert_send::<bool>();
    assert_sync::<i32>();
    assert_sync::<f64>();
    assert_sync::<bool>();
}

// ============================================
// PART 3: COMPOSITE TYPES ARE AUTOMATICALLY SEND/SYNC
// ============================================

struct Point { x: i32, y: i32 }
struct Pair<T, U> { first: T, second: U }

fn composites_are_auto() {
    assert_send::<Point>();
    assert_sync::<Point>();
    assert_send::<Pair<i32, String>>();
    assert_sync::<Pair<i32, String>>();
}

// ============================================
// PART 4: RC IS NEITHER SEND NOR SYNC
// ============================================

fn rc_is_not_thread_safe() {
    // These lines would not compile:
    // assert_send::<Rc<i32>>();
    // assert_sync::<Rc<i32>>();
    //
    // Error: `Rc<i32>` cannot be sent between threads safely
    println!("Rc<i32>: not Send, not Sync");
}

// ============================================
// PART 5: REFCELL IS SEND (IF T: SEND) BUT NOT SYNC
// ============================================

fn refcell_is_send_not_sync() {
    assert_send::<RefCell<i32>>();
    // assert_sync::<RefCell<i32>>(); // would not compile
    println!("RefCell<i32>: Send, not Sync");
}

// ============================================
// PART 6: ARC IS SEND AND SYNC (IF T IS)
// ============================================

fn arc_is_thread_safe() {
    assert_send::<Arc<i32>>();
    assert_sync::<Arc<i32>>();
    assert_send::<Arc<Mutex<i32>>>();
    assert_sync::<Arc<Mutex<i32>>>();
}

// ============================================
// PART 7: MUTEX IS SYNC (IF T: SEND)
// ============================================

fn mutex_is_sync() {
    assert_sync::<Mutex<i32>>();
    assert_send::<Mutex<i32>>();
}

// ============================================
// PART 8: SHARING WITH ARC<MUTEX<T>>
// ============================================

fn share_with_arc_mutex() -> u32 {
    let counter = Arc::new(Mutex::new(0u32));

    let handles: Vec<_> = (0..10)
        .map(|_| {
            let c = Arc::clone(&counter);
            thread::spawn(move || {
                for _ in 0..100 {
                    *c.lock().unwrap() += 1;
                }
            })
        })
        .collect();

    for h in handles {
        h.join().unwrap();
    }

    *counter.lock().unwrap()
}

// ============================================
// PART 9: SHARING WITH ARC<RWLock<T>>
// ============================================

fn share_with_arc_rwlock() -> Vec<i32> {
    let data = Arc::new(RwLock::new(vec![1, 2, 3]));

    let readers: Vec<_> = (0..5)
        .map(|_| {
            let d = Arc::clone(&data);
            thread::spawn(move || {
                let guard = d.read().unwrap();
                guard.iter().sum::<i32>()
            })
        })
        .collect();

    readers.into_iter().map(|h| h.join().unwrap()).collect()
}

// ============================================
// PART 10: MAIN
// ============================================

fn main() {
    primitives_are_send_sync();
    composites_are_auto();
    rc_is_not_thread_safe();
    refcell_is_send_not_sync();
    arc_is_thread_safe();
    mutex_is_sync();

    println!("arc<mutex> counter: {}", share_with_arc_mutex());
    println!("arc<rwlock> sums:   {:?}", share_with_arc_rwlock());
}

The ten parts cover compile-time assertions for primitives, automatic derivation for composite types, the Rc exclusion, the Send-but-not-Sync case of RefCell, the thread-safe Arc and Mutex combinations, and two working examples of shared state. What the session demonstrates is that Send and Sync are not things you implement—they are things the compiler derives from the components you choose, and the choice of component determines thread safety.


Quick Reference

Trait Definitions

TraitDefinitionPractical Meaning
SendSafe to transfer ownership to another threadValue can move across threads
Sync&T is SendShared references can cross threads
Send + SyncBoth propertiesSafe to move and share
!Send + !SyncNeitherSingle-threaded only

Reference Implications

ExpressionSend ifSync if
TT: SendT: Sync
&TT: SyncT: Sync
&mut TT: SendT: Sync
Box<T>T: SendT: Sync

Standard Library Types

TypeSendSyncReason
i32, f64, bool✅✅Primitives
String, Vec<T>✅✅Owned data
Rc<T>❌❌Non-atomic refcount
RefCell<T>✅❌Unsynchronized borrow flag
Cell<T>✅❌Unsynchronized interior mutability
Arc<T>✅✅Atomic refcount
Mutex<T>✅✅Lock-serialized access
RwLock<T>✅✅Lock-serialized access
Raw pointers❌❌No safety guards

Composite Derivation Rules

ComponentResulting Type
All fields SendStruct is Send
All fields SyncStruct is Sync
One field !SendStruct is !Send
One field !SyncStruct is !Sync

Common Thread-Safe Combinations

CombinationSendSyncUse Case
Arc<T>T: Send + SyncT: Send + SyncShared ownership
Arc<Mutex<T>>T: SendT: SendShared mutable state
Arc<RwLock<T>>T: SendT: SendRead-heavy shared state
Mutex<T> aloneT: SendT: SendInterior mutability

Best Practices

✅ Do This:

let x = Arc::new(Mutex::new(0));                              // ✅ Arc for sharing
let y = Arc::clone(&x); thread::spawn(move || ...);          // ✅ clone Arc per thread
assert_send::<MyType>();                                     // ✅ compile-time check
#[derive(Clone)] struct Config { ... }                       // ✅ auto derives
let data = Arc::new(vec![1, 2, 3]);                          // ✅ Send + Sync if T is
let handle = thread::spawn(move || r);                       // ✅ R: Send + 'static

❌ Don’t Do This:

let rc = Rc::new(5); thread::spawn(move || use(rc));         // ❌ Rc not Send
let cell = RefCell::new(0); share_across_threads(cell);      // ❌ RefCell not Sync
unsafe impl Send for MyType {}                               // ❌ unless proven safe
struct Bad { r: Rc<i32> }                                    // ❌ poisons Send/Sync
let x = Mutex::new(0); let y = x; thread::spawn(...);        // ❌ moved, not shared

Common Pitfalls

PitfallWhy It HappensFix
Rc cannot cross threadsNon-atomic refcountUse Arc instead
RefCell cannot be sharedUnsynchronized borrow flagUse Mutex or RwLock
Closure not SendCaptures non-Send typeEnsure all captures are Send
Arc<T> not SendT not Send + SyncMake T satisfy both bounds
Manual impl causes UBIncorrect safety promiseAvoid unless you have a proof
Raw pointer in structRaw pointers are !Send !SyncEncapsulate behind safe abstraction
'static bound failureClosure captures a referenceUse move and owned data

Real-World Examples

1. Arc for shared ownership

let data = Arc::new(vec![1, 2, 3]);
let d = Arc::clone(&data);
thread::spawn(move || { println!("{:?}", d); });

2. Arc for shared mutable state

let counter = Arc::new(Mutex::new(0));
let c = Arc::clone(&counter);
thread::spawn(move || { *c.lock().unwrap() += 1; });

3. Assert Send for API bounds

fn spawn_task<F: Send + 'static>(f: F) { thread::spawn(f); }

4. Struct with Send fields is Send

#[derive(Clone)] struct Config { timeout: u64, name: String }
// Config is automatically Send + Sync

5. RefCell is Send but not Sync

let cell = RefCell::new(0); // Send (if i32), not Sync

6. Mutex is Sync

fn assert_sync<T: Sync>() {} assert_sync::<Mutex<i32>>();

7. Channel endpoints

let (tx, rx) = mpsc::channel::<i32>();
// tx: Send, rx: Send (when T: Send)

8. Raw pointers block auto-derivation

struct Raw { ptr: *const u8 } // !Send, !Sync

9. Arc for read-heavy

let cache = Arc::new(RwLock::new(HashMap::new()));

10. Compile-time safety check

fn is_thread_safe<T: Send + Sync>() {}
is_thread_safe::<Arc<Mutex<Vec<i32>>>>();

Visual

Send and Sync Relationship

┌──────────────────────────────────────────────┐
│  T: Send     →  ownership can move           │
│  T: Sync     →  &T can be shared             │
│                                              │
│  Definition:  T: Sync  ⇔  &T: Send           │
│                                              │
│  ┌───────────────┬───────────┬────────────┐  │
│  │ Type          │ Send?     │ Sync?      │  │
│  ├───────────────┼───────────┼────────────┤  │
│  │ i32, String   │ ✅        │ ✅         │  │
│  │ Rc<T>         │ ❌        │ ❌         │  │
│  │ RefCell<T>    │ ✅        │ ❌         │  │
│  │ Arc<T>        │ ✅*       │ ✅*        │  │
│  │ Mutex<T>      │ ✅        │ ✅         │  │
│  └───────────────┴───────────┴────────────┘  │
│  * when T: Send + Sync                       │
└──────────────────────────────────────────────┘

Auto Trait Derivation

┌──────────────────────────────────────────────┐
│  struct Point { x: i32, y: i32 }             │
│                                              │
│  i32: Send + Sync                            │
│  i32: Send + Sync                            │
│         │                                    │
│         ▼                                    │
│  All fields Send + Sync                      │
│         │                                    │
│         ▼                                    │
│  Point: Send + Sync automatically            │
│                                              │
│  ────────────────────────────────────────    │
│                                              │
│  struct Bad { r: Rc<i32> }                   │
│                                              │
│  Rc<i32>: !Send + !Sync                      │
│         │                                    │
│         ▼                                    │
│  Bad: !Send + !Sync automatically            │
└──────────────────────────────────────────────┘

Thread Safety Matrix

┌──────────────────────────────────────────────┐
│              │ Sync      │ !Sync             │
│  ────────────┼───────────┼───────────────────│
│  Send        │ ✅ Use    │ ⚠️ Move only      │
│              │ Arc<T>    │ (not share)       │
│  ────────────┼───────────┼───────────────────│
│  !Send       │ ⚠️ Rare   │ ❌ Single-thread  │
│              │ (theoretical)                 │
│  ────────────┴───────────┴───────────────────│
│                                              │
│  ✅ Send + Sync: Arc, Mutex, primitives      │
│  ⚠️ Send + !Sync: RefCell, Cell              │
│  ❌ !Send + !Sync: Rc, raw pointers          │
└──────────────────────────────────────────────┘

Arc and Mutex Bounds

┌──────────────────────────────────────────────┐
│  Arc<T>: Send + Sync                         │
│    requires T: Send + Sync                   │
│                                              │
│  Mutex<T>: Send + Sync                       │
│    requires T: Send                          │
│    (no Sync bound: lock serializes access)   │
│                                              │
│  Arc<Mutex<T>>: Send + Sync                  │
│    requires T: Send                          │
│                                              │
│  Channels: Sender<T>, Receiver<T>            │
│    Send when T: Send                         │
└──────────────────────────────────────────────┘

Summary

ItemValue
SendOwnership can transfer across threads
Sync&T can be shared across threads
RelationshipT: Sync ⇔ &T: Send
Auto traitDerived structurally, no manual impl
Unsafe to implementYes, promise to the compiler
Rc<T>!Send, !Sync (non-atomic refcount)
Arc<T>Send + Sync if T: Send + Sync
RefCell<T>Send but !Sync
Mutex<T>Send + Sync if T: Send
Raw pointers!Send, !Sync
Thread spawn boundF: Send + 'static

Key takeaways:

  • Send is about moving, Sync is about sharing. A Send type can move to another thread; a Sync type can be referenced from multiple threads simultaneously .
  • The definition is precise. T: Sync if and only if &T: Send. This relationship explains why Mutex<T> is Sync but RefCell<T> is not .
  • Auto traits derive structurally. If all components are Send, the type is Send. If all are Sync, the type is Sync. You almost never implement them manually .
  • Rc<T> is neither. The non-atomic reference count makes it unsafe to move or share. Arc<T> is the thread-safe replacement .
  • Interior mutability breaks Sync. Cell<T>, RefCell<T>, and UnsafeCell<T> are not Sync because they allow unsynchronized mutation through shared references .
  • Mutex<T> and RwLock<T> restore Sync. They serialize access, making shared references safe even with interior mutability .
  • Manual implementations are unsafe and rare. An unsafe impl Send is a promise the compiler cannot verify. Only write one with a clear safety argument .
  • The compiler rejects unsafe sharing. When you try to send an Rc or share a RefCell, the error is not a bug—it is the type system preventing undefined behavior.

Remember: Send and Sync are the compiler’s thread-safety oracle. Send means a value can move to another thread; Sync means multiple threads can hold references to it. The compiler derives both automatically from a type’s components, so the way to make a type thread-safe is to build it from thread-safe parts. Rc, Cell, and RefCell are excluded because they provide unsynchronized sharing; Arc, Mutex, and RwLock are included because they synchronize it. When you see a Send or Sync error, read it as the compiler telling you that your data structure violates a concurrency invariant—and reach for the synchronized alternative rather than unsafe impl.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!