From 084ac0ecff480e4c0fc68dfe6d50bf590180cb06 Mon Sep 17 00:00:00 2001 From: darkicewolf50 Date: Mon, 17 Mar 2025 15:05:12 -0600 Subject: [PATCH] finished ch16.3 --- ().md | 0 .obsidian/workspace.json | 48 +++--- Concurrency.md | 2 +- Shared State Concurrency.md | 292 ++++++++++++++++++++++++++++++++++++ Sync and Send.md | 1 + 5 files changed, 311 insertions(+), 32 deletions(-) delete mode 100644 ().md create mode 100644 Sync and Send.md diff --git a/().md b/().md deleted file mode 100644 index e69de29..0000000 diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json index 54a0ee1..933e60a 100644 --- a/.obsidian/workspace.json +++ b/.obsidian/workspace.json @@ -35,6 +35,20 @@ "title": "Concurrency" } }, + { + "id": "2a974ca5442d705f", + "type": "leaf", + "state": { + "type": "markdown", + "state": { + "file": "Sync and Send.md", + "mode": "source", + "source": false + }, + "icon": "lucide-file", + "title": "Sync and Send" + } + }, { "id": "3d0ca0b1691c4c2f", "type": "leaf", @@ -63,34 +77,6 @@ "title": "Leaky Reference Cycles" } }, - { - "id": "74da570aae42b3e7", - "type": "leaf", - "state": { - "type": "markdown", - "state": { - "file": "Ref Cell Mutability.md", - "mode": "source", - "source": false - }, - "icon": "lucide-file", - "title": "Ref Cell Mutability" - } - }, - { - "id": "7615e7dc6a30893a", - "type": "leaf", - "state": { - "type": "markdown", - "state": { - "file": "Reference Counter Smart Pointer.md", - "mode": "source", - "source": false - }, - "icon": "lucide-file", - "title": "Reference Counter Smart Pointer" - } - }, { "id": "6ed9f182aa3d5f3e", "type": "leaf", @@ -245,12 +231,13 @@ "command-palette:Open command palette": false } }, - "active": "3d0ca0b1691c4c2f", + "active": "2a974ca5442d705f", "lastOpenFiles": [ "Concurrency.md", + "Sync and Send.md", "Shared State Concurrency.md", - "Smart Pointers.md", "().md", + "Smart Pointers.md", "Simultaneous Code Running.md", "Passing Data Between Threads.md", "Leaky Reference Cycles.md", @@ -273,7 +260,6 @@ "Generics.md", "Lifetimes.md", "2025-02-04.md", - "data_types.md", "does_not_compile.svg", "Untitled.canvas", "Good and Bad Code/Commenting Pratices", diff --git a/Concurrency.md b/Concurrency.md index f6918f8..546b880 100644 --- a/Concurrency.md +++ b/Concurrency.md @@ -46,4 +46,4 @@ Here are the topics that will be covered in this section: - How to create threads to run multiple pieces of code at the same time [Section Link Here](./Simultaneous%20Code%20Running.md) - *Message-passing* concurrency, where channels send messages between threads [Section Link Here](./Passing%20Data%20Between%20Threads.md) - *Shared-state* concurrency, where multiple threads have access to some piece of data [Section Link Here](./Shared%20State%20Concurrency.md) -- The `Sync` and `Send` traits, which extend Rust's concurrency guarantees to use-defined types as well as types provided by the std library \ No newline at end of file +- The `Sync` and `Send` traits, which extend Rust's concurrency guarantees to use-defined types as well as types provided by the std library [Section Link Here](./Sync%20and%20Send.md) \ No newline at end of file diff --git a/Shared State Concurrency.md b/Shared State Concurrency.md index 606f164..ff43c97 100644 --- a/Shared State Concurrency.md +++ b/Shared State Concurrency.md @@ -70,3 +70,295 @@ fn main() { println!("m = {m:?}"); } ``` +Just like with many types, we create a `Mutex` using the associated function `new` + +To access the data in the data inside the mutex, we use the `lock` method to acquire the lock. + +This call will block the current thread so it can't do any work until its our turn to have the lock. + +The call to `lock` would fail if another thread holding the lock panicked. + +In that case no one would ever be able to get the lock. + +Here we chose to `unwrap` and have this thread panic if we are in that situation. + +After the acquire the lock we can treat the return value, named `num` in this case as a mutable reference to the data inside. + +The type system ensures that we acquire a lock before using the value in `m`. + +The type of `m` is `Mutex`, not `i32`, therefore we *must* call `lock` to be able to use the `i32` value. + +The type system won't let us access the inner `i32` otherwise. + +`Mutex` is a smart pointer. + +More accurately, the call to `lock` *returns* a smart pointer called `MutexGuard`, wrapped in a `LockResult` that we handled with the call to `unwrap`. + +The `MutexGaurd` smart pointer implements `Deref` to point at our inner data. + +The smart pointer also has a `Drop` implementation that releases the lock automatically when a `MutexGaurd` goes out of scope. This happens at the end of the inner scope. + +This results in not risking forgetting to release the lock and blocking the mutex form being used by other threads, because the lock releases happens automatically. + +After dropping the lock, we can print the mutex value and see that we are able to change the inner `i32` to 6. + +## Sharing a `Mutex` Between Multiple Threads +Here we will try to share a value between multiple threads suing `Mutex`. + +We will spin up 10 threads and each will increment a counter by 1. + +The counter will go from 0 to 10. + +In this example it will give a compiler error and we will use that to learn a bit more about using `Mutex` and how Rust helps us use it correctly. +```rust +use std::sync::Mutex; +use std::thread; + +fn main() { + let counter = Mutex::new(0); + let mut handles = vec![]; + + for _ in 0..10 { + let handle = thread::spawn(move || { + let mut num = counter.lock().unwrap(); + + *num += 1; + }); + handles.push(handle); + } + + for handle in handles { + handle.join().unwrap(); + } + + println!("Result: {}", *counter.lock().unwrap()); +} +``` +Here we create the `counter` variable which holds an `i32` inside a `Mutex`, just like before. + +Then we create 10 threads by iterating over a range of numbers. + +We use `thread::spawn` and give all the threads the same closure. + +This will moves the counter into the thread, acquires a lock on the `Mutex` by calling the `lock` method, then adding 1 to the value in the mutex. + +Finally when a thread finishes running its closure, `num` will go out of scope and release the lock so another thread can acquire it. + +Here in the main thread we collect all the join handles. + +Then just as we did before we call `join` on each handle to make sure that all the threads finish. + +Once all the threads have finished the main thread will acquire the lock and print the result of the program. + +Here is the output and compiler error +``` +$ cargo run + Compiling shared-state v0.1.0 (file:///projects/shared-state) +error[E0382]: borrow of moved value: `counter` + --> src/main.rs:21:29 + | +5 | let counter = Mutex::new(0); + | ------- move occurs because `counter` has type `Mutex`, which does not implement the `Copy` trait +... +8 | for _ in 0..10 { + | -------------- inside of this loop +9 | let handle = thread::spawn(move || { + | ------- value moved into closure here, in previous iteration of loop +... +21 | println!("Result: {}", *counter.lock().unwrap()); + | ^^^^^^^ value borrowed here after move + | +help: consider moving the expression out of the loop so it is only moved once + | +8 ~ let mut value = counter.lock(); +9 ~ for _ in 0..10 { +10 | let handle = thread::spawn(move || { +11 ~ let mut num = value.unwrap(); + | + +For more information about this error, try `rustc --explain E0382`. +error: could not compile `shared-state` (bin "shared-state") due to 1 previous error +``` +This error message states that the `counter` value was moved in the pervious iteration of the loop. + +Rust is telling us that we can't move the ownership of `counter` into multiple threads. + +We will fix this compilation error with a multiple-ownership method discussed previously. [Ch15](./Smart%20Pointers.md) + +### Multiple Ownership with Multiple Threads +Previously we gave gave a value multiple owners by using the smart pointer `Rc` to create a reference counted value. + +Lets see what happens when we do the same here to see what happens. + +We will wrap the `Mutex` in `Rc` and clone the `Rc` before moving ownership to the thread. +```rust +use std::rc::Rc; +use std::sync::Mutex; +use std::thread; + +fn main() { + let counter = Rc::new(Mutex::new(0)); + let mut handles = vec![]; + + for _ in 0..10 { + let counter = Rc::clone(&counter); + let handle = thread::spawn(move || { + let mut num = counter.lock().unwrap(); + + *num += 1; + }); + handles.push(handle); + } + + for handle in handles { + handle.join().unwrap(); + } + + println!("Result: {}", *counter.lock().unwrap()); +} +``` + +This once again will give us a compilation error, but this is a different compiler error. +``` +$ cargo run + Compiling shared-state v0.1.0 (file:///projects/shared-state) +error[E0277]: `Rc>` cannot be sent between threads safely + --> src/main.rs:11:36 + | +11 | let handle = thread::spawn(move || { + | ------------- ^------ + | | | + | ______________________|_____________within this `{closure@src/main.rs:11:36: 11:43}` + | | | + | | required by a bound introduced by this call +12 | | let mut num = counter.lock().unwrap(); +13 | | +14 | | *num += 1; +15 | | }); + | |_________^ `Rc>` cannot be sent between threads safely + | + = help: within `{closure@src/main.rs:11:36: 11:43}`, the trait `Send` is not implemented for `Rc>`, which is required by `{closure@src/main.rs:11:36: 11:43}: Send` +note: required because it's used within this closure + --> src/main.rs:11:36 + | +11 | let handle = thread::spawn(move || { + | ^^^^^^^ +note: required by a bound in `spawn` + --> file:///home/.rustup/toolchains/1.82/lib/rustlib/src/rust/library/std/src/thread/mod.rs:675:8 + | +672 | pub fn spawn(f: F) -> JoinHandle + | ----- required by a bound in this function +... +675 | F: Send + 'static, + | ^^^^ required by this bound in `spawn` + +For more information about this error, try `rustc --explain E0277`. +error: could not compile `shared-state` (bin "shared-state") due to 1 previous error +``` +Here is the important part to focus on: ``Rc>` cannot be send between threads safely`. + +The compiler also tells us the reason why: the trait `Send` is not implemented `Rc>` + +We will discuss `Send` in the next section. + +For now: it's one of the traits that ensures the types we use with threads are meant for use in concurrent situations. + +`Rc` is not safe to share across threads. + +When `Rc` manages the reference count, it adds to the count for each call to `clone` and subtracts from the count when each clone is dropped. + +This doesn't use any concurrency primitives to make sure that changes to the count can't be interrupted by another thread. + +This could lead to wrong counts, this could cause subtle bugs that could in turn lead to memory leaks or a value being dropped before we are don't with it. + +We need something that is exactly like `Rc` but one that makes changes to the reference count in a thread-safe way. + +### Atomic Reference Counting with `Arc` +`Arc` *is* a type like `Rc` that is safe to use in concurrent situations. + +The *a* stands for *atomic* meaning that it is an *atomically reference counted* type. + +Atomics are an additional kind of concurrency primitive that we won't cover in detail here. + +See the std library documentation for [`std::sync::atomic`](https://doc.rust-lang.org/std/sync/atomic/index.html) for more info. + +At this point know that atomics work like primitive types but are safe to share across threads. + +You may wonder why all primitives types aren't atomic and why std library types aren't implemented to use `Arc` by default. + +This is due the performance penalty that comes with being thread safe. You only want to pay when you really need to. + +If you are just performing operations on values within a single thread, your code can run faster if it doesn't have to enforce the guarantees atomics provide. + +We will update our example to use `Arc`. + +`Arc` and `Rc` have the same API. + +To fix our program by changing the `use` line to call to `new` and the call to `clone`. + +Here is the updated code +```rust +use std::sync::{Arc, Mutex}; +use std::thread; + +fn main() { + let counter = Arc::new(Mutex::new(0)); + let mut handles = vec![]; + + for _ in 0..10 { + let counter = Arc::clone(&counter); + let handle = thread::spawn(move || { + let mut num = counter.lock().unwrap(); + + *num += 1; + }); + handles.push(handle); + } + + for handle in handles { + handle.join().unwrap(); + } + + println!("Result: {}", *counter.lock().unwrap()); +} +``` +This will print the following +``` +Result: 10 +``` +This counts from 0 to 10. + +This doesn't seem very impressive but this taught us a lot about `Mutex` and thread safety. + +This could also be used in a program's structure to do more complex operations than just incrementing a counter. + +Using this strategy, you can divide a calculation into independent parts, split those parts across threads and then use a `Mutex` to have each thread update the final result with its part. + +Note that if you are doing simple numerical operations, there are types simpler than `Mutex` types provided by the [`std::sync::atomic module`](https://doc.rust-lang.org/std/sync/atomic/index.html) + +These types provide safe concurrent, atomic access to primitives types. + +Here we decided to use `Mutex` with a primitive type for this example so we could show how `Mutex` works. + +## Similarities Between `RefCell`/`Rc` and `Mutex`/`Arc` +Notice that `counter` is immutable but we could get a mutable reference to the value inside it. + +This means `Mutex` provides interior mutability, just like how the `Cell` family does. + +In the same way we use `RefCell` in [Ch15](./Smart%20Pointers.md) to allow us to mutate contents inside an `Rc`, we use `Mutex` to mutate contents inside an `Arc`. + +Another thing to notice is that Rust can't protect you form all kinds of logic errors when use use `Mutex`. + +Recall that using `Rc` came with the risk of creating reference cycles, where tow `Rc` values refer to each other, thus causing a memory leak. + +Similarly, `Mutex` comes with the rusk of creating *deadlocks*. + +These occur when an operation needs to lock two resources and two threads each acquired one of the locks, thus causing them to wait for each other forever. + +You can research deadlock mitigation strategies for mutexes in any language and have a go at implementing them in Rust. + +The std library API documentation for `Mutex` and `MutexGuard` offer useful info. + +Next we will talk about the `Send` and `Sync` traits and how we can use them with custom types. + +Go [Here](./Sync%20and%20Send.md) for the next chapter \ No newline at end of file diff --git a/Sync and Send.md b/Sync and Send.md new file mode 100644 index 0000000..2e6dc0f --- /dev/null +++ b/Sync and Send.md @@ -0,0 +1 @@ +# Extensible Concurrency with the `Sync` and `Send` Traits