From 52b86609d70b09c48575f6ce7278b669a83f53a2 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 11 Jan 2024 17:12:56 +0000 Subject: [PATCH 1/6] Use Mutex as an example of interior mutability Mutex is probably more broadly understood by people coming from other languages. --- src/borrowing/interior-mutability.md | 88 ++++++++++++---------------- 1 file changed, 37 insertions(+), 51 deletions(-) diff --git a/src/borrowing/interior-mutability.md b/src/borrowing/interior-mutability.md index a229bfdb8d5f..b5a26d8e85a2 100644 --- a/src/borrowing/interior-mutability.md +++ b/src/borrowing/interior-mutability.md @@ -2,73 +2,59 @@ 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` +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. -[`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. +## `Mutex` -`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. +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. ```rust,editable -use std::cell::RefCell; -use std::rc::Rc; +use std::sync::Mutex; -#[derive(Debug, Default)] -struct Node { - value: i64, - children: Vec>>, +fn handle_hit(hit_counter: &Mutex) { + let mut num_hits = hit_counter.lock().unwrap(); + *num_hits += 1; } -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 current_hit_count(hit_counter: &Mutex) -> u32 { + *hit_counter.lock().unwrap() } 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()); + let hit_counter = Mutex::new(0); + handle_hit(&hit_counter); + println!("{} hits", current_hit_count(&hit_counter)); } ``` +## `Cell` and `RefCell` + +`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. + +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. +
-- 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'`. +- 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. + +- The cell types are more unusual, if simpler. `RefCell` is, in effect, a + `RWMutex` that panics instead of blocking.
From a473dd39de9438662801a403dd6d6257e4c0e4df Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 11 Jan 2024 17:14:41 +0000 Subject: [PATCH 2/6] formatting --- src/borrowing/interior-mutability.md | 30 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/borrowing/interior-mutability.md b/src/borrowing/interior-mutability.md index b5a26d8e85a2..d0bbd82419b3 100644 --- a/src/borrowing/interior-mutability.md +++ b/src/borrowing/interior-mutability.md @@ -6,15 +6,15 @@ minutes: 10 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. +mutability" pattern allows exclusive access behind a shared reference, using a +runtime check to enforce the borrowing rules. ## `Mutex` 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. +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. ```rust,editable use std::sync::Mutex; @@ -37,24 +37,24 @@ fn main() { ## `Cell` and `RefCell` -`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. If there are no references, then 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. +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.
- 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. + 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. -- The cell types are more unusual, if simpler. `RefCell` is, in effect, a +- The cell types are more unusual, if simpler. `RefCell` is, in effect, a `RWMutex` that panics instead of blocking.
From eae282ddc116d2ed76c2945945600f27d45e904a Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Fri, 12 Jan 2024 22:13:59 +0000 Subject: [PATCH 3/6] go back to using RefCell --- src/borrowing/interior-mutability.md | 77 ++++++++++++++++----------- src/concurrency/shared_state/mutex.md | 3 +- 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/src/borrowing/interior-mutability.md b/src/borrowing/interior-mutability.md index d0bbd82419b3..f8c58d610b9e 100644 --- a/src/borrowing/interior-mutability.md +++ b/src/borrowing/interior-mutability.md @@ -4,57 +4,72 @@ 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. +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..f9a06284e47d 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; From e73cc0f91c67b99660d5be936c892e7908af7e14 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Tue, 16 Jan 2024 18:16:46 +0000 Subject: [PATCH 4/6] updates from review --- src/borrowing/interior-mutability.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/borrowing/interior-mutability.md b/src/borrowing/interior-mutability.md index f8c58d610b9e..306ad111e55d 100644 --- a/src/borrowing/interior-mutability.md +++ b/src/borrowing/interior-mutability.md @@ -10,7 +10,7 @@ and wish to update that cache from read-only methods. 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. +while still ensuring safety, typically by performing a runtime check. ## `RefCell` @@ -55,6 +55,10 @@ value. Since there are no references, borrowing rules cannot be broken.
+The main thing to take away from this slide is that Rust provides _safe_ ways to +modify data behind a shared reference. There are a variety of ways to ensure +that safety, and `RefCell` and `Cell` are two of them. + - `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 @@ -64,8 +68,12 @@ value. Since there are no references, borrowing rules cannot be broken. is to allow (and count) many references. But we want to modify the value, so we need interior mutability. +- `Cell` is a simpler means to ensure safety: it has a `set` method that takes + `&self`. This needs no runtime check, but requires moving values, which can + have its own cost. + - Demonstrate that reference loops can be created by adding `root` to - `subtree.children` (don't try to print it!). + `subtree.children`. - 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 From bf1f0038cb8b4528c479936bf6f80ccb6225a7e2 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Tue, 16 Jan 2024 18:27:49 +0000 Subject: [PATCH 5/6] typo --- src/borrowing/interior-mutability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borrowing/interior-mutability.md b/src/borrowing/interior-mutability.md index 306ad111e55d..929ab53b1891 100644 --- a/src/borrowing/interior-mutability.md +++ b/src/borrowing/interior-mutability.md @@ -50,7 +50,7 @@ fn main() { ## `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 +reference to the `Cell`. However, it does not allow any references to the value. Since there are no references, borrowing rules cannot be broken.
From 76e6f458428b2c0ae4ad2497f6f4b41ec154139a Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Tue, 16 Jan 2024 22:47:10 +0000 Subject: [PATCH 6/6] formatting --- src/borrowing/interior-mutability.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borrowing/interior-mutability.md b/src/borrowing/interior-mutability.md index 929ab53b1891..8e09af63a7fe 100644 --- a/src/borrowing/interior-mutability.md +++ b/src/borrowing/interior-mutability.md @@ -50,8 +50,8 @@ fn main() { ## `Cell` `Cell` wraps a value and allows getting or setting the value, even with a shared -reference to the `Cell`. However, it does not allow any references to the -value. Since there are no references, borrowing rules cannot be broken. +reference to the `Cell`. However, it does not allow any references to the value. +Since there are no references, borrowing rules cannot be broken.