diff --git a/week12/parallelism.md b/week12/parallelism.md index cf9da23..034269a 100644 --- a/week12/parallelism.md +++ b/week12/parallelism.md @@ -26,7 +26,20 @@ code { --- -# Parallelism vs. Concurrency +# Today: Parallelism + +- Parallelism vs Concurrency + - Workers, Processes, and Threads +- Multithreading +- Mutual Exclusion +- Atomics +- Message Passing + + +--- + + +# Parallelism vs Concurrency ## Concurrency @@ -39,6 +52,7 @@ code { * **Key difference:** Parallelism utilizes **multiple** workers + --- @@ -46,15 +60,11 @@ code { Parallelism divides tasks among workers. -* In hardwareland, we call these workers **processors** and **cores**. - -* In softwareland... - * "Processors" => Processes - * "Cores" => Threads - -* **Key difference:** Parallelism utilizes **multiple** processors/cores - * Some concurrency models don't! - +* In hardware, we call these workers **processors** and **cores** +* In software, workers are abstracted as **processes** and **threads** + * Processes contain many threads +* Parallelism requires **multiple** workers + * Concurrency models can have any number of workers! --- -# Example: The Main Thread + +# The Main Thread The thread that runs by default is the main thread. ```rust -for i in 1..5 { - println!("working on item {i} from the main thread!"); - thread::sleep(Duration::from_millis(1)); +fn main() { + for i in 1..8 { + println!("Main thread says: Hello {i}!"); + thread::sleep(Duration::from_millis(1)); + } } ``` +* So far, we have only been running code on the main thread! + --- -# Example: Creating a Thread +# Spawning a Thread -We can create ("spawn") more threads with `thread::spawn`: +We can create (spawn) more threads using `std::thread::spawn`. ```rust -let handle = thread::spawn(|| { - for i in 1..10 { - println!("working on item {} from the spawned thread!", i); +fn main() { + let child_handle = thread::spawn(|| { + for i in 1..=8 { + println!("Child thread says: Hello {i}!"); + thread::sleep(Duration::from_millis(1)); + } + }); + + for i in 1..=3 { + println!("Main thread says: Hello {i}!"); + thread::sleep(Duration::from_millis(1)); + } +} +``` + + + + +--- + + +# Spawning a Thread + +```rust +let child_handle = thread::spawn(|| { + for i in 1..=8 { + println!("Child thread says: Hello {i}!"); thread::sleep(Duration::from_millis(1)); } }); -for i in 1..5 { - println!("working on item {i} from the main thread!"); +for i in 1..=3 { + println!("Main thread says: Hello {i}!"); thread::sleep(Duration::from_millis(1)); } ``` -* `thread::spawn` takes a closure as argument - * This is the function that the thread runs +* `thread::spawn` takes a `FnOnce` closure + * The closure will be run on the newly-created thread +* Question: What should this program's output be? + +--- + + +# Possible Output 1 + +Here is one possible program output: + +``` +Main thread says: Hello 1! +Child thread says: Hello 1! +Child thread says: Hello 2! +Main thread says: Hello 2! +Child thread says: Hello 3! +Main thread says: Hello 3! +Child thread says: Hello 4! +Child thread says: Hello 5! +``` + +--- + + +# Possible Output 2 + +Here is another possible program output: + +``` +Main thread says: Hello 1! +Child thread says: Hello 1! +Main thread says: Hello 2! +Main thread says: Hello 3 +``` + + + + --- -# Example: Creating a Thread -What is the output? + + +# Possible Output 3 + +And here is yet another possible program output! + ``` -working on item 1 from the main thread! -working on item 1 from the spawned thread! -working on item 2 from the main thread! -working on item 2 from the spawned thread! -working on item 3 from the main thread! -working on item 3 from the spawned thread! -working on item 4 from the main thread! -working on item 4 from the spawned thread! -working on item 5 from the spawned thread! +Main thread says: Hello 1! +Child thread says: Hello 1! +Main thread says: Hello 2! +Child thread says: Hello 2! +Main thread says: Hello 3! +Child thread says: Hello 3! ``` -* Where did the other four spawned threads go? - * The main thread ended before they executed. - * To prevent this, **join** threads to mandate executed. + +* What's going on here? + + +--- + + +# Multithreaded Code is Non-Deterministic + +* Most of the code we have written this semester has been deterministic +* The only counterexamples have been when we've used random number generators or interacted with I/O +* The execution of multi-threaded code can interleave with each other in undeterminable ways! --- -# Example: Joining Threads +# Multithreaded Code is Non-Deterministic -We join threads when we want to wait for a particular thread to finish execution: +In this example, the child thread can interleave its prints with the main thread. ```rust -let handle = thread::spawn(|| { - for i in 1..10 { - println!("working on item {} from the spawned thread!", i); +let child_handle = thread::spawn(|| { + for i in 1..=8 { + println!("Child thread says: Hello {i}!"); thread::sleep(Duration::from_millis(1)); } }); -for i in 1..5 { - println!("working on item {i} from the main thread!"); +for i in 1..=3 { + println!("Main thread says: Hello {i}!"); thread::sleep(Duration::from_millis(1)); } +``` -handle.join().unwrap(); +* Why doesn't the child thread always print "Hello" 8 times? + + +--- + + +# Process Exit `SIGKILL`s Threads + +* When the main thread finishes execution, the process it belongs to exits +* Once the process exits, all threads in the process are `SIGKILL`ed +* If we want to let our child threads finish, we have to wait for them! + + +--- + + +# Joining Threads + +We can `join` a thread when we want to wait for it to finish. + +```rust +// Spawn the child thread. +let child_handle = thread::spawn(|| time_consuming_function()); + +// Run some code on the main thread. +main_thread_foo(); + +// Wait for the child thread to finish running. +child_handle.join().unwrap(); ``` -* Blocks the current thread until the thread associated with `handle` finishes +* `join` will block the calling thread until the child thread finishes + * In this example, the calling thread is the main thread + --- -# Example: Multithreading Output +# Joining Threads + +If we go back to our original example and add a `join` on the child's handle at the end of the program, then the child thread can run to completion! -What is the output now? ``` -working on item 1 from the main thread! -working on item 2 from the main thread! -working on item 1 from the spawned thread! -working on item 3 from the main thread! -working on item 2 from the spawned thread! -working on item 4 from the main thread! -working on item 3 from the spawned thread! -working on item 4 from the spawned thread! -working on item 5 from the spawned thread! -working on item 6 from the spawned thread! -working on item 7 from the spawned thread! -working on item 8 from the spawned thread! -working on item 9 from the spawned thread! +Main thread says: Hello 1! +Child thread says: Hello 1! +Main thread says: Hello 2! +Child thread says: Hello 2! +Main thread says: Hello 3! +Child thread says: Hello 3! +Child thread says: Hello 4! +Child thread says: Hello 5! +Child thread says: Hello 6! +Child thread says: Hello 7! +Child thread says: Hello 8! ``` -* All ten spawned threads are executed! + --- -# Multithreading +# Example: Multithreaded Drawing Suppose we're painting an image to the screen, and we have eight threads. -* How should we divide the work? - * Divide image into eight regions - * Assign each thread to paint one region +How should we divide up the work between the threads? + +* Divide image into eight regions +* Assign each thread to paint one region * Easy! "Embarrassingly parallel" * Threads don't need to keep tabs on each other @@ -238,20 +364,21 @@ Each thread retires to their cave not too different from modern artists --> + --- -# The Case for Communication +# Example: Multithreaded Drawing -What if our image is more complex? + -* We're painting semi-transparent circles -* Circles overlap and are constantly moving -* The _order_ we paint circles affects the color mixing + - +What if our image is more complex? - +* We could be painting semi-transparent circles +* Circles could overlap and/or could be constantly moving +* The _order_ in which we paint circles changes the colors of pixels --- @@ -278,6 +412,7 @@ Now our threads need to talk to each other! **Problem:** How do threads communicate? **Solutions:** We'll discuss two approaches... + * Approach 1: Shared Memory * Approach 2: Message Passing @@ -291,27 +426,30 @@ Now our threads need to talk to each other! # Approach 1: Shared Memory -For each pixel, create a shared variable `x`: +For each pixel, create a shared variable `x` that represents the number of circles that overlap on this pixel: ```c -static int x = 0; // One per pixel +static int x = 0; // One per pixel. ``` +* _Note that this is C pseudocode: we'll explain the Rust way soon_ * When a thread touches a pixel, increment the pixel's associated `x` * Now each thread knows how many layers of paint there are on that pixel + + --- -# Shared Memory: Data Races +# Approach 1: Shared Memory Are we done? -Not quite... - -* Shared memory is an ingredient for **data races** -* Let's illustrate +* Not quite... +* Shared memory is prone to **data races** + --- # Shared Memory: Data Races -**Third ingredient**: when multiple threads update at once... +**Step 3**: Multiple threads update `x` at the same time... ```c -// Invariant: `x` is total number of times **any** thread has called `update_x` +// Invariant: `x` is total number of times **any** thread has called `update_x`. static int x = 0; static void update_x(void) { - int temp = x; // <- x is INCORRECT - temp += 1; // <- x is INCORRECT - x = temp; // <- x is CORRECT + x += 1; } + // + for (int i = 0; i < 20; ++i) { - spawn_thread(update_x); + spawn_thread(update_x); } ``` @@ -381,25 +529,26 @@ for (int i = 0; i < 20; ++i) { # Shared Memory: Data Races -**Third ingredient**: when multiple threads update at once...they interleave! +**Stpe 3**: When multiple threads update `x` at the same time... they interleave! -| Thread 1 | Thread 2 | -|---------------|---------------| -| temp = x | | -| | temp = x | -| temp += 1 | | -| | temp += 1 | -| x = temp | | -| | x = temp | +| Thread 1 | Thread 2 | +|-------------|-------------| +| `temp = x ` | | +| | `temp = x` | +| `temp += 1` | | +| | `temp += 1` | +| `x = temp` | | +| | `x = temp` | + - +Note that this is just one possible way of incorrect interleaving. +--> --- @@ -407,23 +556,23 @@ A: Next slide # Shared Memory: Data Races -We want `x = 2`, but we get `x = 1`! +We want `x = 2`, but we could get `x = 1`! -| Thread 1 | Thread 2 | -|---------------|---------------| -| Read temp = 0 | | -| | Read temp = 0 | -| Set temp = 1 | | -| | Set temp = 1 | -| Set x = 1 | | -| | Set x = 1 | +| Thread 1 | Thread 2 | +|-----------------|-----------------| +| Read `temp = 0` | | +| | Read `temp = 0` | +| Set `temp = 1` | | +| | Set `temp = 1` | +| Set `x = 1` | | +| | Set `x = 1` | --- -# Shared Memory: Data Races +# Shared Memory: Atomicity We want the update to be **atomic**. That is, other threads cannot cut in mid-update. @@ -442,28 +591,28 @@ We want the update to be **atomic**. That is, other threads cannot cut in mid-up **Not Atomic** -| Thread 1 | Thread 2 | -|---------------|---------------| -| temp = x | | -| | temp = x | -| temp += 1 | | -| | temp += 1 | -| x = temp | | -| | x = temp | +| Thread 1 | Thread 2 | +|-------------|-------------| +| `temp = x` | | +| | `temp = x` | +| `temp += 1` | | +| | `temp += 1` | +| `x = temp` | | +| | `x = temp` |