Fundamentals 8 min read

Mastering Rust’s Send and Sync Traits for Safe Concurrency

Explore how Rust’s Send and Sync marker traits guarantee thread‑safe ownership transfer and shared references, with clear explanations, practical code examples, custom type implementations, common pitfalls, and performance considerations to help you write reliable concurrent programs.

Architecture Development Notes
Architecture Development Notes
Architecture Development Notes
Mastering Rust’s Send and Sync Traits for Safe Concurrency

In Rust's concurrent programming world, the Send and Sync traits act as guardians, ensuring safety through compile‑time type checking. Let’s dive into these two important traits and see how they help us build thread‑safe programs.

Send trait: safety guarantee for cross‑thread transfer

Essence of the Send trait

The Send trait is a marker trait, defined very simply:

<code>pub unsafe auto trait Send { }</code>

When a type implements the Send trait, it indicates that the type can be safely transferred across threads. In other words, if type T implements Send , moving T from one thread to another is safe.

Practical applications of Send

Let’s understand Send through concrete examples:

<code>use std::thread;

// Basic types all implement Send
fn send_basic_types() {
    let number = 42;
    let handle = thread::spawn(move || {
        println!("Number in new thread: {}", number);
    });
    handle.join().unwrap();
}

// Vec<T> is Send if T is Send
fn send_vector() {
    let numbers = vec![1, 2, 3, 4, 5];
    let handle = thread::spawn(move || {
        println!("Vector in new thread: {:?}", numbers);
    });
    handle.join().unwrap();
}
</code>

Types that are not Send

Not all types implement Send . The most typical example is Rc&lt;T&gt; :

<code>use std::rc::Rc;
use std::thread;

fn demonstrate_rc_not_send() {
    let rc = Rc::new(42);
    // The following code would cause a compile error:
    // let handle = thread::spawn(move || {
    //     println!("Rc in new thread: {}", rc);
    // });
}
</code>

Sync trait: safety guarantee for shared references

Definition of Sync

Sync is also a marker trait:

<code>pub unsafe auto trait Sync { }</code>

If type T implements Sync , then shared access via &amp;T across multiple threads is safe. In other words, if T implements Sync , then &amp;T also implements Send .

Sync in practice

Let’s look at some practical uses of Sync :

<code>use std::sync::Arc;
use std::thread;

fn share_data_between_threads() {
    // Arc is a thread‑safe reference‑counted pointer
    let shared_data = Arc::new(42);
    let mut handles = vec![];

    for _ in 0..3 {
        let data_clone = shared_data.clone();
        let handle = thread::spawn(move || {
            println!("Shared data in thread: {}", *data_clone);
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }
}
</code>

Combined use of Send and Sync

Mutex

Mutex&lt;T&gt; is a perfect example showing how Send and Sync cooperate:

<code>use std::sync::Mutex;
use std::sync::Arc;
use std::thread;

fn mutex_example() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..5 {
        let counter_clone = counter.clone();
        let handle = thread::spawn(move || {
            let mut num = counter_clone.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

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

    println!("Final count: {}", *counter.lock().unwrap());
}
</code>

Custom types implementing Send and Sync

When designing your own types, consider whether to implement Send and Sync :

<code>use std::marker::PhantomData;

// A type that is not Send
struct NotSendable {
    _phantom: PhantomData<*const ()>,
}

// A type that is both Send and Sync
#[derive(Debug)]
struct SafeToShare {
    data: i32,
}

unsafe impl Send for SafeToShare {}
unsafe impl Sync for SafeToShare {}
</code>

Best practices and considerations

Writing thread‑safe code

Prefer immutable data.

When mutability is needed, use appropriate synchronization primitives (e.g., Mutex or RwLock ).

Use Arc instead of Rc for sharing data across threads.

Avoid deadlocks.

Common pitfalls

<code>use std::sync::Mutex;

fn deadlock_example() {
    let mutex_a = Mutex::new(1);
    let mutex_b = Mutex::new(2);

    // Potential deadlock code
    let _a = mutex_a.lock().unwrap();
    let _b = mutex_b.lock().unwrap();
    // Solution: acquire locks in a fixed order
}
</code>

Performance considerations

When using Send and Sync , be aware of the following performance‑related issues:

Arc incurs extra atomic‑operation overhead compared to Rc .

Lock contention on Mutex can affect performance.

Overusing synchronization primitives may cause frequent thread blocking.

Conclusion

Rust's Send and Sync traits are the foundation for building safe concurrent programs. They ensure through compile‑time checks that:

Send : a type can safely transfer ownership across threads.

Sync : a type can safely share references across multiple threads.

This design not only detects potential concurrency issues at compile time but also helps us write safer, more reliable concurrent code. By using these two traits wisely, we can fully leverage modern multi‑core processors while avoiding traditional concurrency pitfalls.

Remember, Rust's type system and ownership rules work closely with these traits to provide unique concurrency safety. In real development, we should thoroughly understand and correctly apply them to write code that is both safe and efficient.

ConcurrencyrustThread SafetyMutexsyncARCTraitsSend
Architecture Development Notes
Written by

Architecture Development Notes

Focused on architecture design, technology trend analysis, and practical development experience sharing.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.