From 3837e5d69b72b03c3674891483a2980ac3ea75eb Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Fri, 12 Jan 2024 22:13:59 +0000 Subject: [PATCH] go back to using RefCell --- orig.md | 74 +++++++++++++++++++++++++ src/borrowing/interior-mutability.md | 80 ++++++++++++++++----------- src/concurrency/shared_state/mutex.md | 3 +- 3 files changed, 123 insertions(+), 34 deletions(-) create mode 100644 orig.md diff --git a/orig.md b/orig.md new file mode 100644 index 000000000000..a229bfdb8d5f --- /dev/null +++ b/orig.md @@ -0,0 +1,74 @@ +--- +minutes: 10 +--- + + + +# Interior Mutability + +Rust provides a few safe means of modifying a value given only a shared +reference to that value. All of these replace compile-time checks with runtime +checks. + +## `Cell` and `RefCell` + +[`Cell`](https://doc.rust-lang.org/std/cell/struct.Cell.html) and +[`RefCell`](https://doc.rust-lang.org/std/cell/struct.RefCell.html) implement +what Rust calls _interior mutability:_ mutation of values in an immutable +context. + +`Cell` is typically used for simple types, as it requires copying or moving +values. More complex interior types typically use `RefCell`, which tracks shared +and exclusive references at runtime and panics if they are misused. + +```rust,editable +use std::cell::RefCell; +use std::rc::Rc; + +#[derive(Debug, Default)] +struct Node { + value: i64, + children: Vec>>, +} + +impl Node { + fn new(value: i64) -> Rc> { + Rc::new(RefCell::new(Node { value, ..Node::default() })) + } + + fn sum(&self) -> i64 { + self.value + self.children.iter().map(|c| c.borrow().sum()).sum::() + } +} + +fn main() { + let root = Node::new(1); + root.borrow_mut().children.push(Node::new(5)); + let subtree = Node::new(10); + subtree.borrow_mut().children.push(Node::new(11)); + subtree.borrow_mut().children.push(Node::new(12)); + root.borrow_mut().children.push(subtree); + + println!("graph: {root:#?}"); + println!("graph sum: {}", root.borrow().sum()); +} +``` + +
+ +- If we were using `Cell` instead of `RefCell` in this example, we would have to + move the `Node` out of the `Rc` to push children, then move it back in. This + is safe because there's always one, un-referenced value in the cell, but it's + not ergonomic. +- To do anything with a Node, you must call a `RefCell` method, usually `borrow` + or `borrow_mut`. +- Demonstrate that reference loops can be created by adding `root` to + `subtree.children` (don't try to print it!). +- To demonstrate a runtime panic, add a `fn inc(&mut self)` that increments + `self.value` and calls the same method on its children. This will panic in the + presence of the reference loop, with + `thread 'main' panicked at 'already borrowed: BorrowMutError'`. + +
diff --git a/src/borrowing/interior-mutability.md b/src/borrowing/interior-mutability.md index d0bbd82419b3..f4e86732096d 100644 --- a/src/borrowing/interior-mutability.md +++ b/src/borrowing/interior-mutability.md @@ -4,57 +4,71 @@ minutes: 10 # Interior Mutability -In some situations, exclusive (`mut`) access cannot be represented at compile -time. For example, data might be accessed from multiple threads. The "interior -mutability" pattern allows exclusive access behind a shared reference, using a -runtime check to enforce the borrowing rules. +In some situations, it's necessary to modify data behind a shared (read-only) +reference. For example, a shared data structure might have an internal cache, +and wish to update that cache from read-only methods. -## `Mutex` +The "interior mutability" pattern allows exclusive (mutable) access behind a +shared reference. The standard library provides several ways to do this, all +ensuring safety by performing a runtime check. -One standard type that supports interior mutability is `Mutex` (known as a lock -in some other languages). Many threads can have a shared reference to a `Mutex`, -and the lock operation performs a _runtime_ check that no other thread holds the -lock before granting access to the value inside. +## `RefCell` ```rust,editable -use std::sync::Mutex; +use std::cell::RefCell; +use std::rc::Rc; -fn handle_hit(hit_counter: &Mutex) { - let mut num_hits = hit_counter.lock().unwrap(); - *num_hits += 1; +#[derive(Debug, Default)] +struct Node { + value: i64, + children: Vec>>, } -fn current_hit_count(hit_counter: &Mutex) -> u32 { - *hit_counter.lock().unwrap() +impl Node { + fn new(value: i64) -> Rc> { + Rc::new(RefCell::new(Node { value, ..Node::default() })) + } + + fn sum(&self) -> i64 { + self.value + self.children.iter().map(|c| c.borrow().sum()).sum::() + } } fn main() { - let hit_counter = Mutex::new(0); - handle_hit(&hit_counter); - println!("{} hits", current_hit_count(&hit_counter)); + let root = Node::new(1); + root.borrow_mut().children.push(Node::new(5)); + let subtree = Node::new(10); + subtree.borrow_mut().children.push(Node::new(11)); + subtree.borrow_mut().children.push(Node::new(12)); + root.borrow_mut().children.push(subtree); + + println!("graph: {root:#?}"); + println!("graph sum: {}", root.borrow().sum()); } ``` -## `Cell` and `RefCell` +## `Cell` -`Cell` wraps a value and allows getting or setting the value, even with a shared -reference to the `Cell`. Howerver, it does not allow any references to the -value. If there are no references, then borrowing rules cannot be broken. +`Cell` wraps a value and allows getting or setting the value, even with a +shared reference to the `Cell`. Howerver, it does not allow any references to +the value. Since there are no references, borrowing rules cannot be broken. -The `RefCell` type also provides interior mutability. Its `borrow` method allows -multiple shared references to the data it contains, while its `borrow_mut` -allows a single exclusive reference. It checks the borrowing rules at runtime -and panics if they are violated. +
-Neither `Cell` nor `RefCell` can be shared between threads. +- `RefCell` enforces Rust's usual borrowing rules (either multiple shared + references or a single exclusive reference) with a runtime check. In this case, + all borrows are very short and never overlap, so the checks always succeed. -
+- `Rc` only allows shared (read-only) access to its contents, since its purpose + is to allow (and count) many references. But we want to modify the value, so + we need interior mutability. -- While the fundamentals course doesn't cover concurrency, most students will - have seen a mutex or lock before, and understand that it checks for an - existing lock at runtime, which is the key to interior mutability. +- Demonstrate that reference loops can be created by adding `root` to + `subtree.children` (don't try to print it!). -- The cell types are more unusual, if simpler. `RefCell` is, in effect, a - `RWMutex` that panics instead of blocking. +- To demonstrate a runtime panic, add a `fn inc(&mut self)` that increments + `self.value` and calls the same method on its children. This will panic in the + presence of the reference loop, with + `thread 'main' panicked at 'already borrowed: BorrowMutError'`.
diff --git a/src/concurrency/shared_state/mutex.md b/src/concurrency/shared_state/mutex.md index a5914bf9a35a..81b31f204f0f 100644 --- a/src/concurrency/shared_state/mutex.md +++ b/src/concurrency/shared_state/mutex.md @@ -1,7 +1,8 @@ # `Mutex` [`Mutex`][1] ensures mutual exclusion _and_ allows mutable access to `T` -behind a read-only interface: +behind a read-only interface (another form of [interior +mutability](../../borrowing/interior-mutability)): ```rust,editable use std::sync::Mutex;