Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,11 @@
- [Semantic Confusion](idiomatic/leveraging-the-type-system/newtype-pattern/semantic-confusion.md)
- [Parse, Don't Validate](idiomatic/leveraging-the-type-system/newtype-pattern/parse-don-t-validate.md)
- [Is It Encapsulated?](idiomatic/leveraging-the-type-system/newtype-pattern/is-it-encapsulated.md)
- [RAII](idiomatic/leveraging-the-type-system/raii.md)
- [Mutex](idiomatic/leveraging-the-type-system/raii/mutex.md)
- [Drop Guards](idiomatic/leveraging-the-type-system/raii/drop_guards.md)
- [Drop Bomb](idiomatic/leveraging-the-type-system/raii/drop_bomb.md)
- [Scope Guard](idiomatic/leveraging-the-type-system/raii/scope_guard.md)
- [Typestate Pattern](idiomatic/leveraging-the-type-system/typestate-pattern.md)
- [Typestate Pattern Example](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-example.md)
- [Beyond Simple Typestate](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-advanced.md)
Expand Down
112 changes: 112 additions & 0 deletions src/idiomatic/leveraging-the-type-system/raii.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
minutes: 30
---

# RAII: `Drop` trait

RAII (**R**esource **A**cquisition **I**s **I**nitialization) ties the lifetime
of a resource to the lifetime of a value.

[Rust uses RAII to manage memory](https://doc.rust-lang.org/rust-by-example/scope/raii.html),
and the `Drop` trait allows you to extend this to other resources, such as file
descriptors or locks.

```rust,editable
pub struct File(std::os::fd::RawFd);

impl File {
pub fn open(path: &str) -> Result<Self, std::io::Error> {
// [...]
Ok(Self(0))
}

pub fn read_to_end(&mut self) -> Result<Vec<u8>, std::io::Error> {
// [...]
Ok(b"example".to_vec())
}

pub fn close(self) -> Result<(), std::io::Error> {
// [...]
Ok(())
}
}

fn main() -> Result<(), std::io::Error> {
let mut file = File::open("example.txt")?;
println!("content: {:?}", file.read_to_end()?);
Ok(())
}
```

<details>

- This example shows how easy it is to forget releasing a file descriptor when
managing it manually. The code as written does not call `file.close()`. Did
anyone in the class notice?

- To release the file descriptor correctly, `file.close()` must be called after
the last use — and also in early-return paths in case of errors.

- Instead of relying on the user to call `close()`, we can implement the `Drop`
trait to release the resource automatically. This ties cleanup to the lifetime
of the `File` value.

```rust,compile_fail
impl Drop for File {
fn drop(&mut self) {
println!("release file descriptor automatically");
}
}
```

- Note that `Drop::drop` cannot return errors. Any fallible logic must be
handled internally or ignored. In the standard library, errors returned while
closing an owned file descriptor during `Drop` are silently discarded:
<https://doc.rust-lang.org/src/std/os/fd/owned.rs.html#169-196>

- If both `drop()` and `close()` exist, the file descriptor may be released
twice. To avoid this, remove `close()` and rely solely on `Drop`.
Comment on lines +67 to +68
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having both close and drop is fine, but the close impl needs to mem::forget (or destructure) the File object to prevent drop from also running. That's a normal pattern, especially when close potentially wants to report an error, which can't be done with drop.

I think that's a point worth covering in its own slide. We point out that drop can't return any errors, I think it'd be good to talk about that directly in a slide and demonstrate how you can create a function like close that consumes the object and prevent the normal destructor from running redundantly.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, agreed. There are cases where it is acceptable define both drop (which does best-case cleanup and ignores or merely logs errors) and something like close (which returns an error). I think it would be worthwhile to demonstrate this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like more explicit instructions for the speaker about how the code should evolve up to this point, and how to demonstrate this. In particular the order in which the instructor should add the drop method, remove the close method, how to demonstrate the double-closing of the file etc. The flow can get very messy during the actual presentation if we don't think this through ahead of time.


- When is `Drop::drop` called?

Normally, when the `file` variable in `main` goes out of scope (either on
return or due to a panic), `drop()` is called automatically.

If the file is moved into another function, for example `read_all()`, the
value is dropped when that function returns — not in `main`.

In contrast, C++ runs destructors in the original scope even for moved-from
values.

- The same mechanism powers `std::mem::drop`:

```rust
pub fn drop<T>(_x: T) {}
```
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The instructor can't speak such a long code example. This needs to be put on a separate slide.


You can use it to force early destruction of a value before its natural end of
scope.

- Insert `panic!("oops")` at the start of `read_to_end()` to show that `drop()`
still runs during unwinding.

- There are cases where destructors will not run:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be a good idea to pull this bullet point ("There are cases where destructors will not run") into a separate slide.

- If a destructor itself panics during unwinding, the program aborts
immediately.
- If the program exits with `std::process::exit()` or is compiled with the
`abort` panic strategy, destructors are skipped.
Comment on lines +70 to +97
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think this is worth pulling out into its own slide (I previously commented that, idk if it got lost in the shuffle).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, agreed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good information, but it is not made actionable for the listener. Quoting my original comment:

It is a good tie-in to discuss use cases for drop: it is good for cleaning up things within the scope of a process, but not the right tool for guaranteeing that something happens outside of the process (e.g., on local disk, or in another service in a distributed system).

For example, it is a bad idea to rely exclusively on drop to clean up temporary files: if the program terminates in a way that skips running drop, temporary files will persist, and eventually the computer will run out of space. This can happen if the program crashes or leaks the value whose drop is responsible for deleting the file. In addition to a drop implementation within the program, one also needs a classic unix-style temp file reaper that runs as a separate process.

I suggest adding an example like that to the speaker notes. Otherwise the information "drop is not run when the program exits" is not connected to any real-world bad outcome / architectural mistake.

Comment on lines +93 to +97
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- There are cases where destructors will not run:
- If a destructor itself panics during unwinding, the program aborts
immediately.
- If the program exits with `std::process::exit()` or is compiled with the
`abort` panic strategy, destructors are skipped.
- There are cases where destructors will not run:
- If a destructor itself panics during unwinding, the program aborts
immediately.
- If the object that implements `Drop` is leaked, for example, through `std::mem::forget()`. Leaking is safe in Rust.
- If the program exits with `std::process::exit()` or is compiled with the
`abort` panic strategy, destructors are skipped.


### More to Explore

The `Drop` trait has another important limitation: it is not `async`.

This means you cannot `await` inside a destructor, which is often needed when
cleaning up asynchronous resources like sockets, database connections, or tasks
that must signal completion to another system.

- Learn more:
<https://rust-lang.github.io/async-fundamentals-initiative/roadmap/async_drop.html>
- There is an experimental `AsyncDrop` trait available on nightly:
<https://doc.rust-lang.org/nightly/std/future/trait.AsyncDrop.html>

</details>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary: I like the ideas behind this slide, there is a lot of good content, and it illustrates many important points about type design in Rust.

  • Please split it up according to the comments above.

  • Think about the flow between different points that the speaker should make, and how the code should evolve. Add explicit instructions to the speaker about what to add / remove in the code. Consider adding more slides that snapshot the code in desired state to make it easier for the speaker.

123 changes: 123 additions & 0 deletions src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example on this slide demonstrates using a runtime flag to track if the transaction has been finalized, and then checks that flag to conditionally panic in drop. This is a reasonable approach to take, but it has the drawback of some additional runtime overhead. There's another way to achieve the same thing without any overhead: use mem::forget in commit to directly prevent drop from running.

I'd like to discuss forget somewhere in this section, since it's very relevant when using Drop, especially in cases where you sometimes want to prevent the regular destructor from running. In another comment I suggested splitting out a slide that discusses when drop is and isn't run, discussing forget there might make sense. Otherwise, it wouldn't hurt to have another slide that demonstrates this transaction example using forget instead of the active flag.

Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Drop Bombs: Enforcing API Correctness

Use `Drop` to enforce invariants and detect incorrect API usage. A "drop bomb"
panics if a value is dropped without being explicitly finalized.

This pattern is often used when the finalizing operation (like `commit()` or
`rollback()`) needs to return a `Result`, which cannot be done from `Drop`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before hitting the audience with a long code example, maybe show them a diagram that illustrates a good and a bad method call sequence? (abstractly, just method names, without any code)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not opposed to adding a diagram, but I think the better solution would be to simplify the example enough that we don't need a diagram to explain it. I've already left a handful of notes suggesting changes, with those applied I think the example code would be sufficiently short.


```rust,editable
use std::io::{self, Write};

struct Transaction {
active: bool,
}

impl Transaction {
/// Begin a [`Transaction`].
///
/// ## Panics
///
/// Panics if the transaction is dropped without
/// calling [`Self::commit`] or [`Self::rollback`].
fn start() -> Self {
Self { active: true }
}

fn commit(mut self) -> io::Result<()> {
writeln!(io::stdout(), "COMMIT")?;
self.active = false;
Ok(())
}

fn rollback(mut self) -> io::Result<()> {
writeln!(io::stdout(), "ROLLBACK")?;
self.active = false;
Ok(())
}
Comment on lines +33 to +37
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should remove rollback to simplify this example so that it better fits on the slide. It's also mentioned in the slide text and speaker notes, but I'm not gonna leave comments everywhere to cut down on review noise.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. Alternatively, simplify the body to just a todo!(), and remove all future usages.

}

impl Drop for Transaction {
fn drop(&mut self) {
if self.active {
panic!("Transaction dropped without commit or rollback!");
}
}
}

fn main() -> io::Result<()> {
let tx = Transaction::start();

if some_condition() {
tx.commit()?;
} else {
tx.rollback()?;
}

// Uncomment to see the panic:
// let tx2 = Transaction::start();
Comment on lines +49 to +58
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let tx = Transaction::start();
if some_condition() {
tx.commit()?;
} else {
tx.rollback()?;
}
// Uncomment to see the panic:
// let tx2 = Transaction::start();
let tx = Transaction::start();
// Use `tx` to build the transaction, then commit it.
// Comment out the call to `commit` to see the panic.
tx.commit()?;

Let's simplify this by removing rollback and tx2. That will help the example fit on the slide better.


Ok(())
}

fn some_condition() -> bool {
// [...]
true
}
Comment on lines +62 to +66
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can also be removed once rollback is removed.

```

<details>

- This pattern ensures that a value like `Transaction` cannot be silently
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- This pattern ensures that a value like `Transaction` cannot be silently
- A drop bomb ensures that a value like `Transaction` cannot be silently

dropped in an unfinished state. The destructor panics if neither `commit()`
nor `rollback()` has been called.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
nor `rollback()` has been called.
nor `rollback()` has been called.
- The finalizing operation (like `commit()` or `rollback()`) often consumes the object and thus prevents the user from handling a finalized object.


- A common reason to use this pattern is when cleanup cannot be done in `Drop`,
either because it is fallible or asynchronous.

- This pattern is appropriate even in public APIs. It can help users catch bugs
early when they forget to explicitly finalize a transactional object.

- If a value can be safely cleaned up in `Drop`, consider falling back to that
behavior in Release mode and panicking only in Debug. This decision should be
made based on the guarantees your API provides.

- Panicking in Release builds is a valid choice if silent misuse could lead to
serious correctness issues or security concerns.

## Additional Patterns

- [`Option<T>` with `.take()`](https://doc.rust-lang.org/std/option/enum.Option.html#method.take):
A common pattern inside `Drop` to move out internal values and prevent double
drops.

```rust,compile_fail
impl Drop for MyResource {
fn drop(&mut self) {
if let Some(handle) = self.handle.take() {
// do cleanup with handle
}
}
}
```
Comment on lines +90 to +102
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this deserves its own slide. Having to move out of a field in drop comes up pretty frequently, and this is a common enough pattern that I think it's worth demonstrating explicitly. That can be split out into its own PR if you want.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, agreed.


- [`ManuallyDrop`](https://doc.rust-lang.org/std/mem/struct.ManuallyDrop.html):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this and below is "more to explore" content.

Prevents automatic destruction and gives full manual control. Requires
`unsafe`, so only use when strictly necessary.

- [`drop_bomb` crate](https://docs.rs/drop_bomb/latest/drop_bomb/): A small
utility that panics if dropped unless explicitly defused with `.defuse()`.
Comes with a `DebugDropBomb` variant that only activates in debug builds.

- In some systems, a value must be finalized by a specific API before it is
dropped.

For example, an `SshConnection` might need to be deregistered from an
`SshServer` before being dropped, or the program panics. This helps catch
programming mistakes during development and enforces correct teardown at
runtime.

See a working example in
[the Rust playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=3223f5fa5e821cd32461c3af7162cd55).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please inline the example here. We should prefer to have content we own in the repo, rather than behind links we don't control.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be better to remove this note entirely. The linked code is demonstrating the same thing that the example code in the slide does, I don't think it adds much to the discussion, and I wouldn't want the linked code inlined in the speaker notes as that would add a lot of noise.


</details>
85 changes: 85 additions & 0 deletions src/idiomatic/leveraging-the-type-system/raii/drop_guards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Drop Guards

A **drop guard** in Rust is a temporary _RAII_ guard that executes a specific
action when it goes out of scope.

It acts as a wrapper around a value, ensuring that some cleanup or secondary
behavior happens automatically when the guard is dropped.

One of the most common examples is `MutexGuard`, which represents temporary
exclusive access to a shared resource.
Comment on lines +3 to +10
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
A **drop guard** in Rust is a temporary _RAII_ guard that executes a specific
action when it goes out of scope.
It acts as a wrapper around a value, ensuring that some cleanup or secondary
behavior happens automatically when the guard is dropped.
One of the most common examples is `MutexGuard`, which represents temporary
exclusive access to a shared resource.
A **drop guard** in Rust is a temporary object that performs
some kind of cleanup when it goes out of scope. In the case
of `Mutex`, the `lock` method returns a `MutexGuard` that
automatically unlocks the mutex on drop:

I think more concise wording here would be good to save space for the larger example code on this slide.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, agreed. You can move the longer version to a bullet in the speaker notes.


```rust
#[derive(Debug)]
struct Mutex<T> {
value: std::cell::UnsafeCell<T>,
is_locked: std::sync::atomic::AtomicBool,
}

#[derive(Debug)]
struct MutexGuard<'a, T> {
value: &'a mut T,
mutex: &'a Mutex<T>,
}

impl<T> Mutex<T> {
fn new(value: T) -> Self {
Self {
value: std::cell::UnsafeCell::new(value),
is_locked: std::sync::atomic::AtomicBool::new(false),
}
}

fn lock(&self) -> MutexGuard<'_, T> {
// Acquire the lock and create the guard object.
if self.is_locked.swap(true, std::sync::atomic::Ordering::AcqRel) {
todo!("Block until the lock is released");
}
let value = unsafe { &mut *self.value.get() };
MutexGuard { value, mutex: self }
}
}

impl<'a, T> Drop for MutexGuard<'a, T> {
fn drop(&mut self) {
self.mutex.is_locked.store(false, std::sync::atomic::Ordering::Release);
}
}

fn main() {
let m = Mutex::new(vec![1, 2, 3]);

let mut guard = m.lock();
guard.value.push(4);
guard.value.push(5);
println!("{guard:?}");
}
Comment on lines +13 to +56
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code example is very verbose, I think it'd be better to simplify this. I think we should remove the atomics and UnsafeCell, and simplify this to a dummy mutex that just locks and unlocks (i.e. doesn't actually wrap any data). I think we can also remove the main function since we already showed what using the mutex looks like on the previous slide. My suggestion would be something like this:

#[derive(Debug)]
struct Mutex {
    is_locked: bool,
}

#[derive(Debug)]
struct MutexGuard<'a> {
    mutex: &'a mut Mutex<T>,
}

impl Mutex {
    fn new() -> Self {
        Self {
            is_locked: false,
        }
    }

    fn lock(&mut self) -> MutexGuard<'_> {
        self.is_locked = true;
        MutexGuard { mutex: self }
    }
}

impl<'a> Drop for MutexGuard<'a> {
    fn drop(&mut self) {
        self.mutex.is_locked = false;
    }
}

This is obviously not a realistic mutex, but it's far clearer from the perspective of illustrating the drop guard pattern to students. I think it'd be enough to have a speaker note that points out that this is not accurate to how Mutex is actually implemented.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely agree that the example is too long for a single slide and for the point we're trying to illustrate (drop guards). I like your suggested simplified code. I would like to suggest a few things in addition:

  • add a "// TODO: make atomic for production" comment after "is_locked: bool,"

  • at first removing the value being protected felt like a significant departure from the original mutex. Then I realized that it only felt significant because I was so focused on Rust, but it is actually a pretty common mutex. So, my suggestion is: we should ground this example in how mutexes are implemented in other programming languages, where they are essentially atomic booleans and some kind of waiting primitive. This should be added to the instructor notes. It is however important to explain so that the students understand what we're going for with this example. tl;dr: I think it is better to say that we're trying to implement an equivalent of a mutex say from C++, rather than a simplified Rust mutex.

Something like this:

  • This example shows a C++-style mutex that does not contain the data being protected. This would be deeply non-idiomatic in Rust, but this example only illustrates the core idea of a drop guard. It does not attempt to show a good way of implementing a mutex in Rust.

  • For brevity, we've omitted several features:

    • value being protected,
    • ergonomic access via the Deref and DerefMut traits,
    • a truly blocking .lock() method,
    • correct handling of panics.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine to have a note that this is not a realistic example, but I'd suggest keeping it concise to avoid distracting from the actual point of the slide (i.e demonstrating the guard pattern). I'd prefer not to have a TODO comment in the example code, that just adds noise and isn't really relevant since this is not trying to be an accurate mutex implementation. I also don't think it's necessary to say that this is like a C++ mutex, the distinction between a mutex that wraps data vs a mutex that protects a critical section is entirely tangential to the guard object pattern.

If you want to describe how this example differs from a real mutex, or make a comparison to C++ mutexes, then I'd prefer that those go into a "More to Explore" section at the end of the notes to avoid cluttering the speaker notes section.

```

<details>

- The example above shows a simplified `Mutex` and its associated guard. Even
though it is not a production-ready implementation, it illustrates the core
idea: the guard enforces exclusive access, and its `Drop` implementation
guarantees that the lock is released when the guard goes out of scope.

- A few things are left out for brevity:

- `Deref` and `DerefMut` implementations for `MutexGuard`, which would allow
you to use the guard as if it were a direct reference to the inner value.
- Making `.lock()` truly blocking, so that it waits until the mutex is free
before returning.
- In addition, a `.try_lock()` method could be added to provide a
non-blocking alternative, returning `Option::None` or `Result::Err(...)`
if the mutex is still locked.

- Panics are not explicitly handled in the `Drop` implementation here. In
practice, one can use `std::thread::panicking()` to check if the guard was
dropped during a panic.

- The standard library’s `std::sync::Mutex` uses this to implement
**poisoning**, where a mutex is marked as poisoned if a panic occurs while
holding the lock, since the protected value may now be in an inconsistent
Comment on lines +66 to +82
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove these points about why the example Mutex impl is not accurate, or at least we should move them into a "More to Explore" section to indicate that discussing that is optional. I think talking about how the real Mutex works would distract from the main point about how drop guards work.

state.

</details>
54 changes: 54 additions & 0 deletions src/idiomatic/leveraging-the-type-system/raii/mutex.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Mutex and MutexGuard

In earlier examples, RAII was used to manage concrete resources like file
descriptors. With a `Mutex`, the resource is more abstract: exclusive access to
a value.

Rust models this using a `MutexGuard`, which ties access to a critical section
to the lifetime of a value on the stack.
Comment on lines +3 to +8
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In earlier examples, RAII was used to manage concrete resources like file
descriptors. With a `Mutex`, the resource is more abstract: exclusive access to
a value.
Rust models this using a `MutexGuard`, which ties access to a critical section
to the lifetime of a value on the stack.
In earlier examples, RAII was used to manage concrete resources like file
descriptors. With a `Mutex`, the "resource" is mutable access to a value.
You access the value by calling `lock`, which then returns a `MutexGuard`
which will unlock the `Mutex` automatically when dropped.

A couple suggestions for wording here:

  • I wouldn't say the resource is "abstract", it's just not an external resource the way that a heap allocation or a file handle is.
  • For this slide I think it'd be better to explain the usage of Mutex so that on the next slide we can dig into how MutexGuard works.


```rust
use std::sync::Mutex;

fn main() {
let m = Mutex::new(vec![1, 2, 3]);

let mut guard = m.lock().unwrap();
guard.push(4);
guard.push(5);
println!("{guard:?}");
}
```

<details>

- A `Mutex` controls exclusive access to a value. Unlike earlier RAII examples,
the resource here is not external but logical: the right to mutate shared
data.

- This right is represented by a `MutexGuard`. Only one can exist at a time.
While it lives, it provides `&mut T` access — enforced using `UnsafeCell`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not fully sure what "enforced" means here. UnsafeCell is unsafe because it does not enforce all of its invariants itself.

There are two ways to handle this:

  • remove the reference to UnsafeCell,

  • expand the discussion to make the connection useful to the listeners.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think mentioning UnsafeCell at all makes sense. The thing that's enforcing the "only one guard at a time" rule is the atomic flag that the mutex is using to track if it's locked or not (if the mutex is implemented with something like an AtomicBool, which isn't necessarily the case). UnsafeCell is a primitive for interior mutability, and isn't relevant to our content in this section.


- Although `lock()` takes `&self`, it returns a `MutexGuard` with mutable
access. This is possible through interior mutability: a common pattern for
safe shared-state mutation.

- `MutexGuard` implements `Deref` and `DerefMut`, making access ergonomic. You
lock the mutex, use the guard like a `&mut T`, and the lock is released
automatically when the guard goes out of scope.

- The release is handled by `Drop`. There is no need to call a separate unlock
function — this is RAII in action.

## Poisoning
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend splitting this into its own slide:

  1. mutex.md: Focuses purely on MutexGuard as an RAII wrapper.

  2. mutex_poisoning.md: A new slide dedicated to poisoning. It should demonstrate poisoning actually happening, otherwise it is too abstract for the listeners to catch from a verbal description. For example, this slide could present a should_panic code example that demonstrates a thread panicking while holding a lock, and a second example showing how the next call to lock() returns an Err, along with notes on how to handle that error. It would be a challenge to make that fit on a slide, but the instructor does not need to explain everything as long as we can convince the students that there are two threads using the same mutex: one locks and panics, the second locks and fails.

Poisoning is tangential to the main topic of this section (RAII) so be sure to mark the second slide as skippable if there's time pressure.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should discuss mutex poisoning. As you said, it's tangential to the point of this section, and we don't need to discuss poisoning to demonstrate the guard object pattern. The only reason to mention poisoning is to explain why we do lock().unwrap(), and I think that's an implementation detail that should be glossed over by the instructor. If you think it's necessary to mention poisoning at all, then I'd prefer it be limited to a brief note in the "More to Explore" section.


- If a thread panics while holding the lock, the value may be in a corrupt
state.

- To signal this, the standard library uses poisoning. When `Drop` runs during a
panic, the mutex marks itself as poisoned.

- On the next `lock()`, this shows up as an error. The caller must decide
whether to proceed or handle the error differently.

</details>
Loading
Loading