The simplest way to think about Send and Sync in Rust is that they’re not about preventing data races, but about enabling safe memory sharing between threads.

Let’s watch Send in action. Imagine we have a String that we want to move from one thread to another.

use std::thread;

fn main() {
    let my_string = String::from("hello");

    let handle = thread::spawn(move || {
        // The `move` keyword transfers ownership of `my_string` to this new thread.
        println!("{}", my_string);
    });

    handle.join().unwrap();
}

When thread::spawn takes a move closure, it needs to be sure that any data moved into that closure can be safely transferred to a different thread. String implements Send because it’s designed to be safely moved between threads. The ownership transfer is atomic from the perspective of memory safety. The new thread now owns the String, and the original thread can no longer access it, thus preventing any potential race conditions on that String itself.

Now, let’s look at Sync. This trait is about shared access. If a type T is Sync, it means that &T (an immutable reference to T) can be safely sent to multiple threads simultaneously.

use std::thread;
use std::sync::Arc;

fn main() {
    let counter = Arc::new(0); // Arc<T> is Sync if T is Send

    let mut handles = vec![];

    for _ in 0..10 {
        let counter_clone = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            // Each thread gets a clone of the Arc, which is a shared pointer.
            // Accessing the inner value via `*counter_clone` is safe because
            // Arc<T> is Sync if T is Send.
            // However, we can't *mutate* it here directly without a Mutex.
            println!("Current count (read-only): {}", *counter_clone);
        });
        handles.push(handle);
    }

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

In this example, Arc<i32> is Sync. This means we can have multiple threads holding Arc clones (which are immutable references to the shared i32 data on the heap) and read from it concurrently. The Arc itself manages the reference count atomically, ensuring that the underlying data isn’t deallocated while any thread is still referencing it. The i32 inside is Send (all primitive types are), which is a prerequisite for Arc<i32> to be Sync.

The core problem Send and Sync solve is defining the boundaries for safe concurrency. Send is about ownership transfer, allowing a type to be moved or have its ownership passed to another thread. Sync is about shared immutable access, allowing multiple threads to hold immutable references to a type. Neither trait directly addresses mutable shared access; that’s where synchronization primitives like Mutex or RwLock come in.

The compiler enforces these traits. If you try to send a non-Send type to another thread or share a non-Sync type via an immutable reference across threads, the compiler will give you an error. For instance, raw pointers (*const T and *mut T) are Send but not Sync by default. This is because a raw pointer doesn’t inherently guarantee any safety guarantees. You can have multiple threads holding a *mut T and all trying to write to it, leading to a data race. Rust forces you to opt into this danger explicitly.

The magic behind Send and Sync is that they are marker traits. They don’t have any methods to implement. Their presence or absence on a type (or derived for composite types) is what matters. A type T is Send if all of its fields are Send. Similarly, T is Sync if &T is Send, which boils down to checking if all of its fields are Sync. This recursive definition means that Rust can automatically determine Send and Sync implementations for most standard library types and for your own types composed of these standard types.

The one thing most people don’t realize is that Send and Sync are transitive. If A is Send and B is Send, then (A, B) (a tuple) is also Send. This applies recursively. If you have a struct MyStruct { field1: i32, field2: String }, and both i32 and String are Send, then MyStruct is automatically Send. The same logic applies to Sync. This automatic derivation is a huge part of Rust’s safety guarantees.

The next concept to explore is how Send and Sync interact with unsafe code and custom synchronization primitives.

Want structured learning?

Take the full Rust course →