-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
246: Common Rust Interview Questions on Send and Sync
If you've been prepping for Rust interviews, you've probably seen Send and Sync pop up. They look like simple marker traits, but they're the invisible guardrails that keep your multi-threaded code from exploding. Instead of just reading the documentation, let's actually break some code to see where these boundaries are.
The "Wait, Why Can't I Move This?" Moment
Let's start with something simple. I want to share a reference-counted pointer to a string across two threads. In a single-threaded app, Rc is my go-to. Let's see what happens when I try to push it into a thread.
use std::rc::Rc;
use std::thread;
fn main() {
let data = Rc::new(String::from("Shared Data"));
thread::spawn(move || {
println!("Thread sees: {}", data);
});
}
The compiler screams at me. It says Rc<String> cannot be sent between threads safely. Specifically, it tells me that Rc<String> doesn't implement Send. This is the first big interview concept: Send means "ownership of this type can be transferred to another thread."
Why is Rc not Send? Because Rc uses a non-atomic reference counter. If two threads incremented the counter at the exact same time, we'd have a data race on the counter itself, leading to memory leaks or double-frees. Rust prevents this at compile time. To fix this, I need the atomic version: Arc.
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(String::from("Shared Data"));
let data_clone = Arc::clone(&data);
thread::spawn(move || {
println!("Thread sees: {}", data_clone);
});
}
That compiles. Arc implements Send because it uses atomic operations for the reference count. We've successfully moved ownership of a clone into the new thread.
The Shared Mutation Trap
Now, let's get trickier. Usually, you don't just want to read data; you want to change it. I'll use RefCell for interior mutability, since that's common in single-threaded Rust. Let's wrap it in our Arc and see if that works.
use std::sync::Arc;
use std::cell::RefCell;
use std::thread;
fn main() {
let data = Arc::new(RefCell::new(0));
let data_clone = Arc::clone(&data);
thread::spawn(move || {
*data_clone.borrow_mut() += 1;
}).join().unwrap();
}
The compiler hits me again. This time, it's not complaining that Arc isn't Sendβit's complaining that Arc<RefCell<i32>> isn't Send. But wait, Arc is Send! Why is the whole thing now ineligible?
This is where Sync comes in. The rule is: T is Sync if and only if &T is Send. Arc only implements Send if the inner type T is Sync. Since RefCell is not Sync (because its borrow flag isn't updated atomically), Arc<RefCell> loses its Send status.
Basically, Rust is telling me: "You're trying to share a reference to something that isn't safe to access from multiple threads simultaneously." To fix this, I need a synchronization primitive that is Sync. I'll swap RefCell for Mutex.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(0));
let data_clone = Arc::clone(&data);
thread::spawn(move || {
let mut num = data_clone.lock().unwrap();
*num += 1;
}).join().unwrap();
}
Connecting the Dots for the Interviewer
When an interviewer asks you to explain this, don't just recite definitions. Explain the relationship. I like to think of it as a hierarchy of safety:
- Send: "I can give this object to you, and you can take it to your thread." (Ownership transfer).
- Sync: "We can both hold a reference to this object at the same time across different threads." (Shared reference).
If you're ever stuck, remember that most types are Send and Sync automatically unless they contain something that explicitly isn't. The "villains" are usually Rc (non-atomic counting) and RefCell (non-atomic borrowing). If you see those in a multi-threaded context, you've found the bug.
π Practical Task
Exercise: Fixing the Thread-Unsafe Cache
You are reviewing a piece of code for a shared configuration cache. The previous developer used Rc and RefCell to allow multiple parts of the app to share and update the configuration, but now the app needs to be multi-threaded.
The following code fails to compile. Your task is to modify the ConfigCache implementation so that it can be safely shared and mutated across multiple threads using Arc and Mutex.
use std::rc::Rc;
use std::cell::RefCell;
use std::thread;
use std::collections::HashMap;
struct ConfigCache {
settings: Rc<RefCell<HashMap<String, String>>>,
}
impl ConfigCache {
fn new() -> Self {
Self {
settings: Rc::new(RefCell::new(HashMap::new())),
}
}
fn update(&self, key: &str, value: &str) {
self.settings.borrow_mut().insert(key.to_string(), value.to_string());
}
fn get(&self, key: &str) -> Option<String> {
self.settings.borrow().get(key).cloned()
}
}
fn main() {
let cache = ConfigCache::new();
// This block currently fails to compile
let cache_ref = Rc::clone(&cache.settings);
thread::spawn(move || {
// Logic to update cache from a thread
// You will need to change the types in ConfigCache
// to make this move possible.
}).join().unwrap();
}
Requirements:
- Replace
Rcwith the appropriate thread-safe pointer. - Replace
RefCellwith the appropriate thread-safe interior mutability primitive. - Update the
updateandgetmethods to handle the new locking mechanism. - Ensure the
mainfunction can successfully spawn a thread and move a reference to the cache into it.
There are no comments for now.