---
title: 'Cell, RefCell, Mutex: Interior Mutability'
url: https://doc.liz6.com/en/rust/04-smart-pointers-and-memory/02-Cell-RefCell-Mutex
locale: en
area: rust
tags:
- rust
- smart-pointers-and-memory
date: 2026-06-30
modified: 2026-07-16
description: Rust’s shared references (&T) are immutable by default—Cell/RefCell/Mutex provide varying degrees of interior mutability under the premise of "sharing". Cell is suitable for Copy types (no runtime overhead), RefCell defers borrow checking to runtime (panic on error), and Mutex adds thread-safe locking (Send).
---

# Cell, RefCell, Mutex: Interior Mutability

> Rust’s shared references (`&T`) are immutable by default—Cell/RefCell/Mutex provide varying degrees of interior mutability under the premise of "sharing". Cell is suitable for Copy types (no runtime overhead), RefCell defers borrow checking to runtime (panic on error), and Mutex adds thread-safe locking (Send).

## Origin of the Problem

```rust
let x = 5;
let r = &x;                          // &i32, immutable
// *r = 6;                           // ERROR: &T cannot be modified
```

Rust’s core rule: data cannot be modified through shared references (`&T`). This prevents data races and iterator invalidation. However, sometimes **logically safe modifications are rejected by the rules**—such as caching computed results or internal reference counting. **Interior mutability** allows you to modify internal state while holding `&self`.

## Cell<T>: Replacing the Entire Value (Copy Types Only)

```rust
use std::cell::Cell;
let c = Cell::new(42);
c.set(100);                          // Replace internal value — no &mut self needed
let val = c.get();                   // Copy out
```

`Cell` does not return internal references—`get()` always copies the value (requires `T: Copy`), and `set()` always replaces the entire value. No references leak to the outside, so the borrow checker does not block it. Suitable for simple wrappers of `i32`, `bool`, `enum`, etc.

## RefCell<T>: Runtime Borrow Check

```rust
use std::cell::RefCell;
let rc = RefCell::new(vec![1, 2, 3]);
{
    let mut borrow = rc.borrow_mut(); // runtime: check for no shared borrow
    borrow.push(4);
}                                    // borrow_mut guard dropped → borrow released
let borrow = rc.borrow();            // runtime: OK
```

The rules are the same as the compile-time borrow checker—aliasing XOR mutation—but the check occurs at **runtime**. Violation → `panic!` (not a compile error). `borrow()` returns `Ref<T>` (Deref to &T), and `borrow_mut()` returns `RefMut<T>` (Deref to &mut T). These guards are dropped when they go out of scope, releasing the internal borrow count.

RefCell is not `Sync`—it cannot be shared across threads.

## Mutex<T>: Interior Mutability + Send + Locking

```rust
use std::sync::Mutex;
let m = Mutex::new(42);
let mut guard = m.lock().unwrap();   // Block until lock is acquired
*guard += 1;                         // Access internal value via DerefMut
drop(guard);                         // Release lock (or rely on scope exit)
```

`Mutex` combines `RefCell`'s interior mutability + OS-level locking + `Send` guarantees. Under the hood, it uses `UnsafeCell` + an OS mutex (`pthread_mutex_t` on Linux). `lock()` blocks the current thread until the lock is acquired, and `try_lock()` is non-blocking, immediately returning `Option<MutexGuard>`.

## Comparison of the Three

| | Cell | RefCell | Mutex |
|---|---|---|---|
| Interior Mutability | Y | Y | Y |
| Returns Reference | N (copy only) | Y (Ref/RefMut) | Y (MutexGuard) |
| Conflict Handling | N/A | panic | block |
| Thread-safe | N | N | Y |
| Check Timing | Compile-time (no borrow) | Runtime (panic) | Runtime (block) |

## References

- **Rust Book**: Chapter 15.5
- **Rustonomicon**: UnsafeCell (the primitive behind all interior mutability)

*Keywords: Cell, RefCell, Mutex, interior mutability, UnsafeCell, borrow_mut, runtime borrow check*
