Rust Tutorials: Send and Sync Rules 🎯

beginner
11 min

Rust Tutorials: Send and Sync Rules 🎯

Welcome to the Rust Tutorials on Send and Sync Rules! In this comprehensive guide, we'll dive deep into understanding these crucial concepts for synchronizing data across threads in Rust. Let's get started! 💡

Introduction 📝

In Rust, concurrency is achieved by using threads and async functions. To ensure correct data sharing between them, Rust has a unique system of Send and Sync traits. These traits allow the compiler to check if a type can safely be sent or synchronized across threads.

Send Trait 📝

The Send trait ensures that a type can be safely sent and moved across threads. Rust automatically implements the Send trait for most basic types like integers, strings, and booleans.

To check if a type implements the Send trait, you can use the std::marker::Send trait. For example:

rust
use std::marker::Send; struct MyStruct; impl Send for MyStruct {} // explicitly implementing Send

In the above example, we explicitly declare that MyStruct can be sent across threads. If you don't provide an implementation, Rust assumes that types which do not contain any references, closures, or non-Send types implement the Send trait by default.

Sync Trait 📝

The Sync trait guarantees that a type is safe for concurrent access by multiple threads. Unlike the Send trait, Rust does not automatically implement the Sync trait for most types.

To check if a type implements the Sync trait, you can use the std::marker::Sync trait. For example:

rust
use std::marker::Sync; struct MyStruct; impl Sync for MyStruct {} // explicitly implementing Sync

In this example, we explicitly declare that MyStruct is safe for concurrent access. Similarly, you can implement Sync for your custom types when necessary.

Send and Sync Rules 📝

  • A type is Send if it only contains Send types, and does not implement Trait Objects or contain any inner references.
  • A type is Sync if it is a primitive type, implements both Send and Sync, or is a reference to a Send type.

Implications for Structs 📝

To make your structs Send and Sync-compatible, follow these guidelines:

  1. Mark fields as Send and Sync where necessary: If a field contains non-Send or non-Sync data, you must mark it as such.
rust
struct MyStruct { data: i32, non_send_ref: &'static SomeType, } impl Send for MyStruct {} impl Sync for MyStruct {}
  1. Implement Send and Sync for the struct: If your struct contains references, closures, or non-Send or non-Sync types, you must explicitly implement the Send and Sync traits.

Practical Examples 💡

Now that we've covered the theory, let's look at practical examples of custom structs that implement Send and Sync traits.

Example 1: A simple thread-safe counter 📝

rust
use std::sync::Arc; use std::sync::mpsc::SyncSender; use std::thread; struct Counter { count: Arc<i32>, sender: SyncSender<i32>, } impl Counter { fn new(initial_value: i32) -> Self { let count = Arc::new(initial_value); let sender = mpsc::sync_channel(1).0; Counter { count, sender } } fn increment(&self) { *self.count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) } fn send_count(&self) { self.sender.send(self.count.clone()).unwrap(); } } fn main() { let counter = Counter::new(0); thread::spawn(move || { for _ in 0..100 { counter.increment(); counter.send_count(); thread::sleep(std::time::Duration::from_millis(10)); } }); // Main thread reads and prints the counter value periodically for _ in 0..100 { if let Ok(count) = counter.count.clone().load(std::sync::atomic::Ordering::SeqCst) { println!("Counter: {}", count); } thread::sleep(std::time::Duration::from_millis(20)); } }

Example 2: A simple thread-safe cache 💡

rust
use std::sync::Arc; use std::sync::RwLock; use std::thread; struct Cache { cache: Arc<RwLock<Option<i32>>>, } impl Cache { fn new() -> Self { Cache { cache: Arc::new(RwLock::new(None)), } } fn set(&self, key: i32, value: i32) { let mut cache = self.cache.write().unwrap(); *cache = Some(value); } fn get(&self, key: i32) -> Option<i32> { let cache = self.cache.read().unwrap(); cache.clone() } } fn main() { let cache = Cache::new(); thread::spawn(move || { cache.set(1, 42); thread::sleep(std::time::Duration::from_secs(2)); }); thread::spawn(move || { let value = cache.get(1); if let Some(value) = value { println!("Got value: {}", value); } else { println!("Cache miss"); } }); }

In the above examples, both Counter and Cache are thread-safe because they implement Send and Sync traits, ensuring correct data sharing across threads.

Quiz 🎯

Quick Quiz
Question 1 of 1

Which of the following traits is responsible for ensuring that a type is safe for concurrent access by multiple threads?

By now, you should have a solid understanding of Send and Sync traits in Rust and how to use them to create thread-safe data structures. Happy coding! 💡