Estimated time: 1 day
Rust ownership model allows only one owner of a value. However, there are situations when multiple ownership is required, and it's important to understand how this can be accomplished.
The key piece is to put a value behind a smart pointer, so the pointer itself can be cloned many times (thus allowing multiple owners), but is pointing always to the same value (thus sharing a value). In Rust there is a Rc
("reference counted") smart pointer for this purpose, and Arc
("atomic reference counted") for use in multiple threads. Both automatically destroy a value once there are no references left.
The code below won't compile as a
is owned by x
and moved to a heap before is passed to y
:
struct Val(u8);
let a = Val(5);
let x = Box::new(a);
let y = Box::new(a);
error[E0382]: use of moved value: `a`
--> src/main.rs:6:22
|
5 | let x = Box::new(a);
| - value moved here
6 | let y = Box::new(a);
| ^ value used here after move
|
= note: move occurs because `a` has type `Val`, which does not implement the `Copy` trait
However, Rc
allows that:
let a = Rc::new(Val(5));
let x = Rc::clone(&a); // does not clone original value,
let y = Rc::clone(&a); // but rather produces new reference to it
The Rc
, however, should be used wisely as won't deallocate memory on references cycle which is exactly what a memory leak is. Rust is unable to prevent memory leaks at compile time (though makes hard to produce them). If it's still required to have a references cycle, you should use a Weak
smart pointer ("weak reference") in combination with Rc
. Weak
allows to break a references cycle as can refer to a value that has been dropped already (returns None
in such case).
For better understanding Rc
/Weak
purpose, design, limitations and use cases read through:
- Rust Book: 15.4. Rc, the Reference Counted Smart Pointer
- Rust Book: 15.6. Reference Cycles Can Leak Memory
- Official
std::rc
docs
Rust memory safety is based on the following rules (known as "borrowing rules"):
Given an object
T
, it is only possible to have one of the following:
- Having several immutable references (
&T
) to the object (also known as aliasing).- Having one mutable reference (
&mut T
) to the object (also known as mutability).
However, quite often there are situations where these rules are not flexible enough and it's required to have multiple references to a value and yet mutate it. Cell
and RefCell
encapsulate mutability inside (thus called "interior mutability") and provide interface which can be used through common shared references (&T
). Mutex
and RwLock
serve the same purpose, but in a multi-threaded context.
These containers allow to overcome Rust borrowing rules and track borrows at runtime (so called "dynamic borrowing"), which, obviously, leads to less safe code as compile-time errors become runtime panics. That's why one should use Cell
/RefCell
wisely and only as a last resort.
For better understanding Cell
/RefCell
purpose, design, limitations and use cases read through:
- Rust Book: 15.5. RefCell and the Interior Mutability Pattern
- Official
std::cell
docs - Paul Dicker: Interior mutability patterns
- David Tolnay: Accurate mental model for Rust’s reference types
The most spread case is a combination of two previous: Rc<RefCell<T>>
(or Arc<Mutex<T>>
). This allows to mutate a value by multiple owners.
A real-world example would be a database client object: it must be mutable, as mutates its state under-the-hood (opens network connections, manages database sessions, etc), yet we need to own it in multiple places of our code, not a single one.
The following articles may explain you this concept better:
- Manish Goregaokar: Wrapper Types in Rust: Choosing Your Guarantees
- Alexandre Beslic: Rust, Builder Pattern, Trait Objects,
Box<T>
andRc<T>
There is a simple rule for omitting deadlocks with Mutex
/RwLock
(applicable for panics with Cell
/RefCell
types too):
Locking scopes must not intersect in any way.
The following example explains why deadlocks happen:
let owner1 = Arc::new(Mutex::new("string"));
let owner2 = owner1.clone();
let value = owner1.lock.unwrap();
// owner2 locking scope intersects with owner1 lock's scope.
let value = owner2.lock.unwrap();
Let's remove the intersection:
let owner1 = Arc::new(Mutex::new("string"));
let owner2 = owner1.clone();
{
let value = owner1.lock.unwrap();
// No intersection as owner1 locking scope ends here.
}
{
let value = owner2.lock.unwrap();
}
That's why, usually, you should omit to expose Rc<RefCell<T>>
(or Arc<Mutex<T>>
) in API's, but rather make them an inner implementation detail. Doing this way you have full control over all locking scopes inside your methods (no scope can expand to outside), so ensure that no intersection will happen, and expose a totally safe API.
#[derive(Clone)]
struct SharedString(Arc<Mutex<String>>);
impl SharedString {
fn mutate_somehow(&self) {
let mut val = self.lock.unwrap();
*val = "another string"
}
}
let owner1 = SharedString(Arc::new(Mutex::new("string")));
let owner2 = owner1.clone();
// We are mutating the same value here,
// but no locking scopes intersection may happen by design.
// Such API will never deadlock or panic
// due to runtime violation of borrowing rules.
owner1.mutate_somehow();
owner2.mutate_somehow();
And even when there is no possibility to hide lock guards behind API boundary, it may be feasible to try encoding the described property via type system, using zero-sized wrapper types on guards. See the following articles for examples and design insights:
Write a GlobalStack<T>
collection which represents a trivial unsized stack (may grow infinitely) and has the following semantics:
- can be mutated through multiple shared references (
&GlobalStack<T>
); - cloning doesn't clone data, but only produces a pointer, so multiple owners mutate the same data.
After completing everything above, you should be able to answer (and understand why) the following questions:
- What is shared ownership? Which problem does it solve? Which penalties does it have?
- What is interior mutability? Why is it required in Rust? In what price it comes?
- Is it possible to write a custom type with interior mutability without using
std
? Why? - What is shared mutability? Which are its common use-cases?
- How can we expose panic/deadlock-free API to users when using interior mutability?