Course contentsShow
← Rust
Lesson 1139 of 1,91010. Concurrency PrimitivesFree lesson

Unsafe Cell for Custom Synchronization

You will build your own primitive locks by leveraging the interior mutability allowed by UnsafeCell.

In this lesson, you will learn how to leverage UnsafeCell<T> as the fundamental building block for constructing your own thread-safe synchronization primitives from scratch.

The Problem with Shared Mutability

In Rust, the compiler enforces the rule: aliasing XOR mutation. If you have a shared reference &T, you cannot modify the data. This is great for safety, but it prevents you from implementing primitives like a Mutex or a Spinlock. These structures need to hold shared references to data that they internally mutate based on whether a lock is currently acquired.

UnsafeCell<T> is the only type in Rust that allows "interior mutability." By wrapping data in an UnsafeCell, you opt out of the compiler's strict alias-mutation rules. However, you assume the responsibility for maintaining memory safety.

Building a Simple Spinlock

To build a custom synchronization primitive, you pair UnsafeCell with an atomic flag. The flag manages the "locked/unlocked" state, while the UnsafeCell provides the raw, mutable access to the protected data.

use std::cell::UnsafeCell;
use std::sync::atomic::{AtomicBool, Ordering};

pub struct SimpleLock<T> {
    locked: AtomicBool,
    data: UnsafeCell<T>,
}

// We must manually implement Sync to allow sharing across threads
unsafe impl<T: Send> Sync for SimpleLock<T> {}

impl<T> SimpleLock<T> {
    pub fn new(val: T) -> Self {
        Self {
            locked: AtomicBool::new(false),
            data: UnsafeCell::new(val),
        }
    }

    pub fn lock(&self) -> &mut T {
        // Spin until we successfully swap 'false' with 'true'
        while self.locked.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed).is_err() {}
        
        // Safety: We now own the lock, so we can return a mutable reference
        unsafe { &mut *self.data.get() }
    }
}

In this example, self.data.get() returns a raw pointer (*mut T). Because we gate this access behind the AtomicBool logic, we ensure that only one thread can ever obtain that &mut T at a time. The UnsafeCell effectively hides the mutation from the borrow checker, allowing us to build the logic that keeps the program sound.

Key Takeaway: UnsafeCell is the "escape hatch" that allows you to safely provide shared mutability, provided you implement external synchronization logic to prevent data races.