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
A recorded source review is available for 3 of 8 answers. Each reviewed answer lists its date and scope.
01What are Send and Sync, and how does the compiler derive them automatically?
middle
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:
- Structural derivation — the auto trait propagates through fields: a struct of
Sendfields is itselfSend. Raw pointers (*const T,*mut T) deliberately opt out of both, which strips the traits from types built on them. unsafe impl— if you uphold thread safety manually (e.g. wrapping a raw pointer in correct synchronization), you may writeunsafe impl Send for MyType {}. That's an assertion you're responsible for: the compiler can't check it.- Opting out explicitly — a negative impl (
impl !Send for T) is nightly-only; that's howRcis marked insidestd. On stable you strip the trait with a marker field likePhantomData<*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.
02Why does RwLock<T> require T: Send + Sync while Mutex<T> only requires T: Send?
senior
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:
Mutex<T>: SyncwhenT: Send— access is always serialized; two threads never hold&Tat the same time. The value is effectively only moved between threads over time → onlySendis needed.RwLock<T>: SyncwhenT: Send + Sync— multiple readers hold&Tin parallel. That's literally the definition ofSync(sharing&T), so it's added to the requirements.Sendin 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.
03Give an example of a type that is Send but not Sync — and one that is Sync but not Send.
senior
Short answer: Cell<T> / RefCell<T> are Send when T: 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:
Send + !Sync→Cell/RefCell— moving is exclusive, hence safe. A shared&Cellfrom two threads would allow simultaneous writes without synchronization — so!Sync.Sync + !Send→MutexGuard—&guardonly yields&T, safe to share (henceSync). But on many OSespthread_mutex_unlockmust be called by the thread that owns the lock, so the guard cannot be moved to another thread and dropped there →!Send.!Send + !Sync→Rc— for completeness: a non-atomic reference count is unsafe both to move and to share.
| Type | Send | Sync |
|---|---|---|
Cell<T> / RefCell<T> |
when T: Send |
no |
MutexGuard<'_, T> |
no | yes (when T: Sync) |
Arc<T> |
when T: Send + Sync |
when T: Send + Sync |
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.
04What's the difference between a data race and a race condition? Which does Rust prevent?
middle
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:
- Data race — two threads touch the same cell without synchronization and one writes. Impossible in safe Rust: the type system won't grant
&mutalongside any other access. - 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.
- 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.
05What is Mutex poisoning? What does .lock() return after another thread panicked while holding the lock?
middle
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:
- Why — a panic mid-mutation may have broken invariants. Poisoning stops you from silently continuing with broken state.
- What's returned —
lock()yieldsLockResult<MutexGuard<T>>=Result<_, PoisonError<_>>. Common practice is.unwrap()if the invariant is critical, or a deliberate recovery viainto_inner()plus resetting the flag withMutex::clear_poison()(stable since 1.77). - Who does NOT poison —
parking_lot::Mutexandtokio::sync::Mutexhave no poisoning mechanism at all; theirlock()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.
06What are scoped threads (std::thread::scope) for, and how do they let you borrow non-'static data?
middle
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:
- The
spawnproblem —thread::spawnrequires a'staticclosure because the thread may outlive the current stack frame. So you end up cloning into anArcormove-ing ownership. - The
scopefix — the scope joins all threads at its boundary, so references to locals are guaranteed valid for the whole lifetime of the threads. - Result — you can read
&datain parallel from several threads, and even hand out disjoint&mutslices without extraArcallocations or data copies to transfer ownership. Creating threads still has a cost.
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: treating scoped threads as free. Borrowing can remove Arc and copies, but thread creation, scheduling, and join still have costs and synchronization. Shared mutable state still needs protection; disjoint &mut regions can avoid a lock when the relevant Send/Sync requirements hold.
07What atomic memory orderings exist (Relaxed, Acquire, Release, AcqRel, SeqCst), and how do you choose?
senior
Short answer: Relaxed preserves atomicity and the modification order of that atomic, but does not by itself publish accesses to other memory. Release/Acquire enable publication when the acquire reads the corresponding release write or its release sequence. AcqRel combines both sides for read-modify-write. SeqCst additionally orders all SeqCst operations together.
In depth:
| Ordering | Purpose |
|---|---|
| Relaxed | an independent counter that does not signal readiness of other data |
| Release | publish preceding accesses with a write |
| Acquire | observe published accesses through the matching read |
| AcqRel | acquire on the read and release on the write of a read-modify-write operation |
| SeqCst | the relevant acquire/release guarantees plus a single order of SeqCst operations |
- Equal numeric values are insufficient: which write the acquire operation read matters.
- Not every ordering is valid for every operation: an ordinary load cannot use
Release/AcqRel, and a store cannot useAcquire/AcqRel; compare-exchange has a separate allowed failure ordering. SeqCstsimplifies some reasoning but does not make an arbitrary algorithm correct or replace protection of non-atomic data.- Cost depends on architecture and operation. Weaker ordering does not guarantee a speedup; weaken only with a correctness argument and measurements.
⚠️ Common mistake: choosing by “Relaxed is faster” or pairing any acquire read with any release. Explain which operations need a happens-before relationship and how it is established.
08Compare std::sync::mpsc, crossbeam-channel and tokio::sync::mpsc — when do you pick which?
middle
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:
std::sync::mpsc— in the standard library, blockingrecv(), 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.crossbeam-channel— MPMC (clonableReceiver), aselect!macro over several channels; the de-facto choice for multi-threaded (non-async) code when you need those capabilities.tokio::sync::mpsc—.awaitpoints 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
Source links provide further reading. Where a recorded technical review is available, the answer shows its date and scope. Tool selections use direct provider links and contain no affiliate placements. Sources, features, and hiring formats can change.
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.