Sling Academy
Home/Rust/Sync and Send Traits in Rust: Ensuring Safe Cross-Thread Data Access

Sync and Send Traits in Rust: Ensuring Safe Cross-Thread Data Access

Last updated: January 06, 2025

Anyone who's developed multithreaded applications in Rust has likely encountered the need for thread-safe access to data. Rust's ownership model can make multithreading safe and efficient, and in this article, we'll explore how Rust handles this with its Sync and Send traits.

Understanding Send and Sync

At the core of Rust's concurrency model are the Send and Sync traits, both of which are marker traits. Unlike traits that define methods, these provide information about the types that implement them.

Send Trait

The Send trait allows for transferring ownership of types across thread boundaries. If a type implements the Send trait, it means its ownership can be safely sent from one thread to another. Most types in Rust are Send by default. For example, primitive types like integers and floats are Send, as are compound types like tuples, structs, and even references, provided the things they reference are also Send.

fn is_send() {}

fn main() {
    is_send::();   // OK, because i32 is Send
    is_send::<&str>();  // OK, because &str (static reference) is Send
}

Sync Trait

The Sync trait signifies that a type is safe to share between threads. If a type is Sync, it means that it is safe for multiple threads to have references to it and access it concurrently. As a rule of thumb, for a type to be Sync, it must either be immutable or manage its internal mutability in a thread-safe manner using mechanisms like Mutex or RwLock.

fn is_sync() {}

fn main() {
    is_sync::();   // OK, because i32 is Sync
    is_sync::<&str>();  // OK, because immutable references are Sync
    // Note: Types that contain non-Sync components (like raw pointers) would not pass here
}

Creating Thread-Safe Types

To create types that are safe to share and send across threads, you often need to combine Send and Sync. Let's design a simple counter that's safe to be shared across threads using std::sync constructs.

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

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

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

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

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

In this example, Arc is used to safely share the counter across threads, while Mutex ensures only one thread can update the counter at a time. Both Arc and Mutex implement Send and Sync.

The Role of the Compiler

Rust's compiler plays a key role by enforcing the rules of these traits. If you try to send a non-Send type to a new thread, the Rust compiler will throw a compilation error. Similarly, sharing non-Sync references across threads will lead to compilation errors, stopping you from making dangerous multithreading errors.

Custom Types and Unsafe Code

In rare cases, you may need to define custom types where

the automatic implementation doesn't apply, particularly when dealing with raw pointers or complex memory manipulations. In such cases, you can opt into Send or Sync by implementing them manually but be cautious. Incorrect implementation may lead to race conditions or undefined behavior.

unsafe impl Send for MyType {}
unsafe impl Sync for MyType {}

Even though this is possible, try to use Rust's concurrency abstractions whenever possible, and only delve into unsafe territory if absolutely necessary.

Conclusion

The Send and Sync traits are integral to writing safe concurrent applications in Rust. They leverage Rust's powerful type system to ensure that only data meant for multithreaded access is allowed across threads. By understanding and correctly implementing these traits, developers can build robust multithreaded applications without sacrificing safety for speed.

Next Article: Sharing Data Across Threads in Rust Using Arc and Mutex

Previous Article: Creating Threads in Rust with std::thread::spawn

Series: Concurrency in Rust

Rust

You May Also Like

  • E0557 in Rust: Feature Has Been Removed or Is Unavailable in the Stable Channel
  • Network Protocol Handling Concurrency in Rust with async/await
  • Using the anyhow and thiserror Crates for Better Rust Error Tests
  • Rust - Investigating partial moves when pattern matching on vector or HashMap elements
  • Rust - Handling nested or hierarchical HashMaps for complex data relationships
  • Rust - Combining multiple HashMaps by merging keys and values
  • Composing Functionality in Rust Through Multiple Trait Bounds
  • E0437 in Rust: Unexpected `#` in macro invocation or attribute
  • Integrating I/O and Networking in Rust’s Async Concurrency
  • E0178 in Rust: Conflicting implementations of the same trait for a type
  • Utilizing a Reactor Pattern in Rust for Event-Driven Architectures
  • Parallelizing CPU-Intensive Work with Rust’s rayon Crate
  • Managing WebSocket Connections in Rust for Real-Time Apps
  • Downloading Files in Rust via HTTP for CLI Tools
  • Mocking Network Calls in Rust Tests with the surf or reqwest Crates
  • Rust - Designing advanced concurrency abstractions using generic channels or locks
  • Managing code expansion in debug builds with heavy usage of generics in Rust
  • Implementing parse-from-string logic for generic numeric types in Rust
  • Rust.- Refining trait bounds at implementation time for more specialized behavior