Skip to content

Latest commit

 

History

History
 
 

1_3_rc_cell

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

Task 1.3: Shared ownership and interior mutability

Shared ownership

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 it 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, even 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 it can refer to a value that has already been dropped(returns None in such case).

For better understanding Rc/Weak purpose, design, limitations and use cases read through:

Interior mutability

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:

Shared mutability

The most spread case is a combination of two previous: Rc<RefCell<T>> (or Arc<Mutex<T>>). This allows to mutate a value via 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:

Avoiding panics and deadlocks

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. This way you have full control over all locking scopes inside your methods (no scope can leak outside), which will help to 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:

Task

Estimated time: 1 day

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.

Implement tests for GlobalStack<T>.

Questions

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 does it come?
  • 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?