We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Atomics, Send/Sync and the Memory Model step 10 of 12
Build a spinlock from AtomicBool and UnsafeCell
Write a real lock, from the two primitives everything else is made of.
pub struct SpinLock<T> { /* AtomicBool + UnsafeCell<T> */ }
impl<T> SpinLock<T> {
pub fn new(value: T) -> Self;
pub fn lock(&self) -> SpinLockGuard<'_, T>;
pub fn into_inner(self) -> T;
}
pub fn spin_counter(increments: usize, workers: usize) -> usize
SpinLockGuard<'_, T> must implement Deref, DerefMut and Drop.
spin_counter shares a SpinLock<Vec<u64>> holding four counters between
workers scoped threads; worker w performs increments increments,
spreading them over the four slots. Return the total, which is
increments * workers. Treat workers == 0 as 1.
The protected value is a Vec, not an integer, deliberately — a Vec has
a destructor and a heap allocation, so a wrong Drop or a wrong Deref fails
loudly instead of quietly.
Why Mutex<T> contains its data
You have been told that the data living inside the lock is a design insight. Building one is where you find out it is not a stylistic choice — it is forced.
UnsafeCell<T> is the only way in the entire language to get a &mut T
from a &T. It is not a library type with a clever trick; the compiler knows
about it and switches off the no-aliasing assumption for anything reachable
through it. Cell, RefCell, Mutex, RwLock, atomic::* — every one of
them is an UnsafeCell plus a discipline that makes it sound.
So a lock that did not contain its data would have nowhere to put the
UnsafeCell, and no way to hand out the &mut. The guard is the only way
out of the cell, and taking the lock is the only way to get a guard. That is
the whole architecture, and it is why you cannot forget to lock in Rust.
The protocol
Lock:
while self.locked
.compare_exchange_weak(false, true, Acquire, Relaxed)
.is_err()
{
spin_loop();
}
Unlock, in the guard’s Drop:
self.lock.locked.store(false, Release);
The Acquire/Release pair is not decoration. It is exactly what makes the
previous holder’s writes visible to the next one. Whoever unlocks does a
Release store; whoever locks does an Acquire read-modify-write that
observes it; the happens-before edge that creates is what makes it sound to
hand out a &mut T to data another thread just wrote. Weaken either and the
lock still excludes correctly and stops publishing correctly — the worst kind
of bug, and the one this grader cannot see.
Use compare_exchange_**weak** inside the loop: it may fail spuriously and
you are looping anyway, so the strong form’s internal retry is pure waste.
The failure ordering can be Relaxed, because a failed attempt tells you
nothing you are going to act on.
The starter has two bugs and only one of them is a compile error
First, the gate:
error[E0277]: `UnsafeCell<Vec<u64>>` cannot be shared between threads safely
= help: within `SpinLock<Vec<u64>>`, the trait `Sync` is not implemented
for `UnsafeCell<Vec<u64>>`
UnsafeCell is deliberately not Sync — of course it is not, it is
unsynchronised interior mutability by definition. Your lock is synchronised,
and the compiler cannot know that. So you make the promise:
unsafe impl<T: Send> Sync for SpinLock<T> {}
Justify the bound before you write it. Why T: Send and not T: Sync?
Because only one thread at a time can reach the data — that is what the lock
is for — so the T is transferred between threads, never shared. Transfer
is exactly what Send licenses. This is the same bound std::sync::Mutex
carries, for the same reason, and it is worth being able to explain rather
than copy. (RwLock needs T: Send + Sync precisely because several readers
hold &T at once.)
Second, and the toolchain will not tell you: the starter’s lock() is
test-then-set.
while self.locked.load(Acquire) { spin_loop(); }
self.locked.store(true, Release);
Two threads can both read false before either writes true, and both then
believe they hold the lock. Checking and claiming must be one indivisible
operation, which is precisely what compare_exchange is. This bug does not
produce a data race the compiler can see — you already promised Sync — it
produces lost updates and a wrong total.
Why a spinlock is a bad default
A blocked thread here does not sleep; it burns a core asking “is it free yet?” On an oversubscribed machine the thread holding the lock may be descheduled while every spinner keeps its core busy denying it a chance to run — the lock is held for a whole scheduling quantum and throughput collapses.
That is why the work here is bounded (workers <= 8, modest increments), and
it is why real implementations spin briefly and then park. std::sync::Mutex
does exactly that. Spinning is right when the critical section is a handful of
instructions and contention is low; the moment either assumption fails, it is
the worst option available.
The lints, honestly
clippy::missing_spin_loop and clippy::empty_loop do enforce
std::hint::spin_loop() in a wait loop — but they were observed to fire on a
local atomic and not when the atomic is reached through an Arc or a
reference, which is the shape here. So the gate will not catch it for you.
Write the hint anyway: it emits a pause/yield instruction that cuts power
draw and speeds the eventual exit.
clippy::mut_from_ref is deny-by-default and would catch a fn get(&self) -> &mut T that hands out a &mut from a shared reference without a guard —
the exact soundness hole this design avoids by making deref_mut take
&mut self.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.