Skip to Content
Course content

246: Common Rust Interview Questions on Send and Sync

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 Rc with the appropriate thread-safe pointer.
  • Replace RefCell with the appropriate thread-safe interior mutability primitive.
  • Update the update and get methods to handle the new locking mechanism.
  • Ensure the main function can successfully spawn a thread and move a reference to the cache into it.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.