Skip to content
Languages

8 Rust Send/Sync and Atomics Interview Questions and Answers

This focused guide turns RecallDeck’s curated Rust Send/Sync and Atomics material into 8 interview-ready questions. Answer each one before opening the explanation, then use the examples and edge cases to repair anything vague or incomplete.

9 min read8 detailed answersReviewed Aug 24, 2026
What to remember

Answer from a language model, not from memorized syntax: name the mechanism, show a small example, and explain the production consequence.

Question set

8 detailed answers

01

What are Send and Sync, and how does the compiler derive them automatically?

Short answer: Send means ownership of a value can be safely moved to another thread; Sync means &T can be safely shared across threads (by definition T: Sync&T: Send). These are auto traits: the compiler implements them structurally — a type is Send/Sync if all its fields are. You write nothing by hand.

In depth:

  1. Structural derivation — the auto trait propagates through fields: a struct of Send fields is itself Send. Raw pointers (*const T, *mut T) deliberately opt out of both, which strips the traits from types built on them.
  2. unsafe impl — if you uphold thread safety manually (e.g. wrapping a raw pointer in correct synchronization), you may write unsafe impl Send for MyType {}. That's an assertion you're responsible for: the compiler can't check it.
  3. Opting out explicitly — a negative impl (impl !Send for T) is nightly-only; that's how Rc is marked inside std. On stable you strip the trait with a marker field like PhantomData<*const ()> (!Send + !Sync).
Type Send Sync Why
Rc<T> no no non-atomic reference count
Arc<T> yes yes atomic count (when T: Send + Sync)
Cell<T> / RefCell<T> yes no unsynchronized interior mutability
Mutex<T> yes yes synchronization inside (when T: Send)
*mut T no no a raw pointer offers no guarantees

⚠️ Common mistake: thinking Send and Sync are about a type's "thread-safety in general". Send is about moving ownership between threads, Sync about shared access via a reference; these are distinct guarantees, and a type can have one without the other.

02

Why does RwLock<T> require T: Send + Sync while Mutex<T> only requires T: Send?

Short answer: A Mutex hands out exclusive access to exactly one thread at any moment — only ownership "travels" across the thread boundary, so T: Send suffices. A RwLock can hand out several read guards simultaneously to different threads, which is a shared &T across threads — so it additionally needs T: Sync.

In depth:

  1. Mutex<T>: Sync when T: Send — access is always serialized; two threads never hold &T at the same time. The value is effectively only moved between threads over time → only Send is needed.
  2. RwLock<T>: Sync when T: Send + Sync — multiple readers hold &T in parallel. That's literally the definition of Sync (sharing &T), so it's added to the requirements.
  3. Send in both — needed because the value itself can be moved into another thread along with the lock.
 Mutex: only one in the critical section
   thread A ──[lock]──► &mut T ──[unlock]──► thread B ──[lock]──► ...
            (exclusive, over time — Send is enough)

 RwLock: many readers AT ONCE
   thread A ─┐
   thread B ─┼─► &T  (shared access right now → needs Sync)
   thread C ─┘

⚠️ Common mistake: claiming both locks require the same bounds. The difference is exactly RwLock's concurrent reads: parallel &T held by several threads is the very sharing that requires Sync.

03

Give an example of a type that is Send but not Sync — and one that is Sync but not Send.

Short answer: Cell<T> / RefCell<T> are Send but !Sync: unsynchronized interior mutability is safe to move to another thread but not to share by reference (two threads would write without synchronization → data race). MutexGuard<'_, T> is the reverse — Sync (when T: Sync) but !Send: the guard is safe to share via &, but the same thread that locked it must release the lock.

In depth:

  1. Send + !SyncCell/RefCell — moving is exclusive, hence safe. A shared &Cell from two threads would allow simultaneous writes without synchronization — so !Sync.
  2. Sync + !SendMutexGuard&guard only yields &T, safe to share (hence Sync). But on many OSes pthread_mutex_unlock must be called by the thread that owns the lock, so the guard cannot be moved to another thread and dropped there → !Send.
  3. !Send + !SyncRc — for completeness: a non-atomic reference count is unsafe both to move and to share.
Type Send Sync
Cell / RefCell yes no
MutexGuard<'_, T> no yes (when T: Sync)
Arc<T> yes yes
Rc<T> no no

⚠️ Common mistake: assuming Send and Sync always come as a pair. Cell and MutexGuard are the canonical counterexamples, and interviewers often ask about exactly these.

04

What's the difference between a data race and a race condition? Which does Rust prevent?

Short answer: A data race is unsynchronized concurrent access to the same memory where at least one access is a write; that's UB. A race condition is a logic bug from unlucky operation ordering (e.g. TOCTOU). Rust statically rules out data races (via ownership + Send/Sync), but not race conditions or deadlocks: those compile just fine.

In depth:

  1. Data race — two threads touch the same cell without synchronization and one writes. Impossible in safe Rust: the type system won't grant &mut alongside any other access.
  2. Race condition — correctness depends on timing. The classic is TOCTOU: you check a condition, and by the time you act it's no longer true. The compiler doesn't catch this.
  3. What Rust does NOT guarantee — freedom from deadlocks, memory leaks, logic races, or overflows. "Fearless concurrency" is about the absence of data races, not the absence of all concurrency bugs.
Property Data race Race condition
Essence unsync access + write dependence on ordering
Result UB wrong result
Example two threads write x check balance, then debit
Rust catches it yes, at compile time no

⚠️ Common mistake: claiming "Rust prevents races" without qualification. It guarantees no data races, but not race conditions or deadlocks — those still have to be designed away by hand.

05

What is Mutex poisoning? What does .lock() return after another thread panicked while holding the lock?

Short answer: If a thread panics while holding a MutexGuard, the std mutex is marked "poisoned". After that, .lock() returns Err(PoisonError) — a signal that the protected data may be left in an inconsistent state. .unwrap() on that Result re-panics; you can still recover the data via PoisonError::into_inner().

In depth:

  1. Why — a panic mid-mutation may have broken invariants. Poisoning stops you from silently continuing with broken state.
  2. What's returnedlock() yields LockResult<MutexGuard<T>> = Result<_, PoisonError<_>>. Common practice is .unwrap() if the invariant is critical, or a deliberate recovery via into_inner() plus resetting the flag with Mutex::clear_poison() (stable since 1.77).
  3. Who does NOT poisonparking_lot::Mutex and tokio::sync::Mutex have no poisoning mechanism at all; their lock() returns the guard directly.
use std::sync::Mutex;

let data = Mutex::new(0);
let guard = match data.lock() {
    Ok(g) => g,
    // deliberately continue, aware the invariant may be broken
    Err(poisoned) => poisoned.into_inner(),
};

⚠️ Common mistake: thinking poisoning "breaks" the mutex forever. The data is reachable via into_inner(), and parking_lot/tokio don't poison at all — don't carry std's behavior over to them automatically.

06

What are scoped threads (std::thread::scope) for, and how do they let you borrow non-'static data?

Short answer: std::thread::scope guarantees that every thread spawned inside it finishes (is joined) before the scope exits. That lets the compiler prove borrows outlive the threads, so you can pass ordinary &T / &mut T to stack data — no Arc, no 'static requirement. Stable since Rust 1.63.

In depth:

  1. The spawn problemthread::spawn requires a 'static closure because the thread may outlive the current stack frame. So you end up cloning into an Arc or move-ing ownership.
  2. The scope fix — the scope joins all threads at its boundary, so references to locals are guaranteed valid for the whole lifetime of the threads.
  3. Result — you can read &data in parallel from several threads, and even hand out disjoint &mut slices without touching the heap.
let mut v = vec![1, 2, 3];

// spawn: needs 'static — this would NOT compile with &v
// thread::spawn(|| println!("{v:?}")); // error: v isn't 'static

std::thread::scope(|s| {
    s.spawn(|| println!("reading: {v:?}"));  // plain &v — ok
    s.spawn(|| println!("again: {}", v.len()));
}); // both threads are already joined here

v.push(4); // mutable again — the borrows are released

⚠️ Common mistake: reaching for Arc<Mutex<_>> where the data lives on the stack and threads are short-lived. For that pattern thread::scope gives you borrowing with zero allocations or synchronization.

07

What atomic memory orderings exist (Relaxed, Acquire, Release, SeqCst) and when do you use each?

Short answer: Relaxed gives only atomicity of the operation, with no ordering guarantees relative to other accesses — fine for independent counters. Acquire/Release form a pair for a "publish → consume" hand-off (flags, lock implementations). SeqCst imposes a single global order over all SeqCst operations — a safe default, but the most expensive.

In depth:

  1. Relaxed — atomicity only, no happens-before. The classic case is incrementing a metric/counter where only the final sum matters.
  2. Release (on the write) + Acquire (on the read) — everything written before the Release becomes visible to the thread that did an Acquire of the same value. This is the data-publication mechanism and the basis of mutexes.
  3. SeqCst — like Acquire/Release, plus a single total order over all SeqCst ops. Easier to reason about but costlier; rarely needed (e.g. by some lock-free algorithms).
Ordering Guarantee Typical use
Relaxed atomicity only counters, metrics
Acquire sees writes before the paired Release reading a flag/lock
Release publishes its writes writing a flag/unlock
SeqCst single global order safe default, rare algorithms

⚠️ Common mistake: slapping SeqCst everywhere "just in case". The interviewer wants a justified choice: Relaxed is enough for a counter, an Acquire/Release pair for a hand-off, and a needless SeqCst just pays for barriers with no benefit.

08

Compare std::sync::mpsc, crossbeam-channel and tokio::sync::mpsc — when do you pick which?

Short answer: std::sync::mpsc is a blocking multi-producer, single-consumer (MPSC) channel with no select. crossbeam-channel is MPMC (multiple consumers too), with select! and a clonable Receiver. tokio::sync::mpsc is an async channel with backpressure, meant for use inside async tasks. The choice is dictated by the execution model: blocking code or async.

In depth:

  1. std::sync::mpsc — in the standard library, blocking recv(), many producers / one consumer, no multiplexing across channels. Since Rust 1.67 it's internally a port of crossbeam-channel, so "std is slow" is an outdated argument.
  2. crossbeam-channel — MPMC (clonable Receiver), a select! macro over several channels; the de-facto choice for multi-threaded (non-async) code when you need those capabilities.
  3. tokio::sync::mpsc.await points instead of blocking; bounded capacity provides backpressure; used inside an async runtime.
Channel Model select Where to use
std::sync::mpsc MPSC, blocking no simple multi-threaded code
crossbeam-channel MPMC, blocking yes (select!) threaded code needing select/MPMC
tokio::sync::mpsc MPSC, async yes (tokio::select!) inside async tasks

⚠️ Common mistake: calling a blocking recv() (std/crossbeam) inside an async function. It blocks the runtime's worker thread and can stall the whole task pool — in async code use tokio::sync::mpsc (or spawn_blocking).

Source notes

References and review policy

RecallDeck’s interview answers are editorial material, reviewed against maintained official documentation where a primary reference is available. Tool selections use direct provider links and contain no affiliate placements. Features can change after the review date.

From reading to recall

Practice the full interview loop.

RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.

Start studying

Keep going

Languages103 min

Top 100 Go Interview Questions and Answers

Prepare with 100 Go interview questions and detailed answers on slices, interfaces, concurrency, runtime, testing, distributed systems, and reliability.

100 detailed answers
RecallDeck Interview Library

Detailed answers from the same curated interview deck, organized for search, study, and durable recall.

RSS