From d39b23f8a004f2a75822e40cf7fa49dc2b8e87e1 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Wed, 14 Jun 2023 10:00:24 +0100 Subject: [PATCH 1/4] Add threads and closures. --- training-slides/src/closures.md | 150 +++++++++++++++++++ training-slides/src/spawning-threads.md | 182 ++++++++++++++++++++++++ 2 files changed, 332 insertions(+) diff --git a/training-slides/src/closures.md b/training-slides/src/closures.md index 713d5db..b2fffe4 100644 --- a/training-slides/src/closures.md +++ b/training-slides/src/closures.md @@ -1 +1,151 @@ # Closures + +## Rust's Function Traits + +* `trait FnOnce` +* `trait FnMut: FnOnce` +* `trait Fn: FnMut` + +Note: + +* Instances of FnOnce can only be called once. +* Instances of FnMut can be called repeatedly and may mutate state. +* Instances of Fn can be called repeatedly without mutating state. +* `Fn` (a trait) and `fn` (a function pointer) are different! + +## These traits are implemented by: + +* Function Pointers +* Closures + +## Function Pointers + +```rust +fn add_one(x: usize) -> usize { + x + 1 +} + +fn main() { + let ptr: fn(usize) -> usize = add_one; + println!("ptr(5) = {}", ptr(5)); +} +``` + +## Closures + +* Defined with `||` +* Most basic kind, are just function pointers + +```rust +fn main() { + let clos: fn(usize) -> usize = |x| x + 5; + println!("clos(5) = {}", clos(5)); +} +``` + +## Capturing + +* Closures can capture their environment. +* Now it's an anonymous `struct`, not a `fn` +* It implements `Fn` + +```rust +fn main() { + let increase_by = 1; + let clos = |x| x + increase_by; + println!("clos(5) = {}", clos(5)); +} +``` + +## Capturing Mutably + +* Closures can capture their environment by mutable reference +* Now it implements `FnMut` + +```rust +fn main() { + let mut total = 0; + let mut update = |x| total += x; + update(5); + update(5); + println!("total: {}", total); +} +``` + +Note: + +The closure is dropped before the `println!`, making `total` accessible again (the &mut ref stored in the closure is now gone). +If you try and call `update()` after the `println!` you get a compile error. + +## Capturing by transferring ownership + +```rust +fn main() { + let items = vec![1, 2, 3, 4]; + let update = move || { + for item in items { + println!("item is {}", item); + } + }; + update(); + // println!("items is {:?}", items); +} +``` + +## But why? + +* But why is this useful? +* It makes iterators really powerful! + +```rust [] +fn main() { + let items = [1, 2, 3, 4, 5, 6]; + let n = 2; + for even_number in items.iter().filter(|x| (**x % n) == 0) { + println!("{} is even", even_number); + } +} +``` + +## Cleaning up + +It's also very powerful if you have something you need to clean up. + +1. You do some set-up +2. You want do some work (defined by the caller) +3. You want to clean up after. + +```rust [] +fn setup_teardown(f: F) -> T where F: FnOnce(&mut Vec) -> T { + let mut state = Vec::new(); + println!("> Setting up state"); + let t = f(&mut state); + println!("< State contains {:?}", state); + t +} +``` + +## Cleaning up + +```rust [] +fn setup_teardown(f: F) -> T where F: FnOnce(&mut Vec) -> T { + let mut state = Vec::new(); + println!("> Setting up state"); + let t = f(&mut state); + println!("< State contains {:?}", state); + t +} + +fn main() { + setup_teardown(|s| s.push(1)); + setup_teardown(|s| { + s.push(1); + s.push(2); + s.push(3); + }); +} +``` + +Note: + +In release mode, all this code just gets inlined. diff --git a/training-slides/src/spawning-threads.md b/training-slides/src/spawning-threads.md index b42538c..94bfdbd 100644 --- a/training-slides/src/spawning-threads.md +++ b/training-slides/src/spawning-threads.md @@ -1 +1,183 @@ # Spawning Threads and Scoped Threads + +## Platform Differences - Windows + +* On Windows, a *Process* is just an address space, and it has one *Thread* by default. +* You can start more *Threads* + +```c +HANDLE CreateThread( + /* [in, optional] */ LPSECURITY_ATTRIBUTES lpThreadAttributes, + /* [in] */ SIZE_T dwStackSize, + /* [in] */ LPTHREAD_START_ROUTINE lpStartAddress, // <<-- function to run in thread + /* [in, optional] */ __drv_aliasesMem LPVOID lpParameter, // <<-- context for thread function + /* [in] */ DWORD dwCreationFlags, + /* [out, optional] */ LPDWORD lpThreadId +); +``` + +## Platform Differences - POSIX + +* On POSIX, a *Process* includes one thread of execution. +* You can start more *Threads*, typically using the POSIX Threads API + +```c +int pthread_create( + pthread_t *restrict thread, + const pthread_attr_t *restrict attr, + void *(*start_routine)(void *), // <<-- function to run in thread + void *restrict arg // <<-- context for thread function +); +``` + +## Rusty Threads + +The Rust [thread API](https://doc.rust-lang.org/std/thread/) looks like this: + +```rust ignore +pub fn spawn(f: F) -> JoinHandle +where + F: FnOnce() -> T + Send + 'static, + T: Send + 'static, +``` + +## Using spawn + +* You *could* pass a function to `std::thread::spawn`. +* In almost all cases you pass a *closure* + +```rust [] +use std::{thread, time}; + +fn main() { + let thread_handle = thread::spawn(|| { + thread::sleep(time::Duration::from_secs(1)); + println!("I'm a thread"); + }); + + thread_handle.join().unwrap(); +} +``` + +## Why no context? + +* There's no `void* p_context` argument, because *closures* can *close-over* local variables. + +```rust [] +use std::thread; + +fn main() { + let number_of_loops = 5; // on main's stack + let thread_handle = thread::spawn(move || { + for _i in 0..number_of_loops { // captured by value, not reference + println!("I'm a thread"); + } + }); + + thread_handle.join().unwrap(); +} +``` + +Note: + +Try changing this *move* closure to a regular referencing closure. + +## Context lifetimes + +* However, the thread might live forever... + +```rust [] +use std::{sync::Mutex, thread}; + +fn main() { + let buffer: Mutex> = Mutex::new(Vec::new()); + let thread_handle = thread::spawn(|| { + for i in 0..5 { + // captured by reference, does not live long enough + // buffer.lock().unwrap().push(i); + } + }); + thread_handle.join().unwrap(); + let locked_buffer = buffer.lock(); + println!("{:?}", &locked_buffer); +} + +``` + +## Making context live forever + +If a thread can live forever, we need its context to live just as long. + +```rust [] +use std::{sync::{Arc, Mutex}, thread}; + +fn main() { + let buffer = Arc::new(Mutex::new(Vec::new())); + let thread_buffer = buffer.clone(); + let thread_handle = thread::spawn(move || { + for i in 0..5 { + thread_buffer.lock().unwrap().push(i); + } + }); + thread_handle.join().unwrap(); + let locked_buffer = buffer.lock().unwrap(); + println!("{:?}", &locked_buffer); +} +``` + +## Tidying up the handle + +* In Rust, functions take *expressions* +* Blocks are expressions... + +```rust ignore +let thread_buffer = buffer.clone(); +let thread_handle = thread::spawn( + move || { + for i in 0..5 { + thread_buffer.lock().unwrap().push(i); + } + } +); +``` + +## Tidying up the handle + +* In Rust, functions take *expressions* +* Blocks are expressions... + +```rust ignore +let thread_handle = thread::spawn({ + let thread_buffer = buffer.clone(); + move || { + for i in 0..5 { + thread_buffer.lock().unwrap().push(i); + } + } +}); +``` + +Note: + +This clearly limits the visual scope of the `thread_buffer` variable, to match the logical scope caused by the fact it is transferred by value into the closure. + +## Scoped Threads + +As of 1.63, we can say the threads will all have ended before we carry on our calling function. + +```rust [] +use std::{sync::Mutex, thread}; + +fn main() { + let buffer = Mutex::new(Vec::new()); + thread::scope(|s| { + s.spawn(|| { + for i in 0..5 { + buffer.lock().unwrap().push(i); + } + }); + }); + let locked_buffer = buffer.lock().unwrap(); + println!("{:?}", &locked_buffer); +} +``` From a06d3a24f08e999937d714124c7f219f5d099c0f Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Wed, 14 Jun 2023 10:29:16 +0100 Subject: [PATCH 2/4] Update training-slides/src/spawning-threads.md Co-authored-by: Tanks Transfeld --- training-slides/src/spawning-threads.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/training-slides/src/spawning-threads.md b/training-slides/src/spawning-threads.md index 94bfdbd..5906018 100644 --- a/training-slides/src/spawning-threads.md +++ b/training-slides/src/spawning-threads.md @@ -61,7 +61,7 @@ fn main() { ## Why no context? -* There's no `void* p_context` argument, because *closures* can *close-over* local variables. +There's no `void* p_context` argument, because *closures* can *close-over* local variables. ```rust [] use std::thread; From e21dfb9858331622fc952d8d7df6c7e6d9be49cc Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Wed, 14 Jun 2023 10:29:23 +0100 Subject: [PATCH 3/4] Update training-slides/src/spawning-threads.md Co-authored-by: Tanks Transfeld --- training-slides/src/spawning-threads.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/training-slides/src/spawning-threads.md b/training-slides/src/spawning-threads.md index 5906018..0821b10 100644 --- a/training-slides/src/spawning-threads.md +++ b/training-slides/src/spawning-threads.md @@ -84,7 +84,7 @@ Try changing this *move* closure to a regular referencing closure. ## Context lifetimes -* However, the thread might live forever... +However, the thread might live forever... ```rust [] use std::{sync::Mutex, thread}; From 8fff621ba43014e00c96696a66cdcb19dc7a9002 Mon Sep 17 00:00:00 2001 From: "Jonathan Pallant (Ferrous Systems)" Date: Wed, 14 Jun 2023 10:32:23 +0100 Subject: [PATCH 4/4] Missing caption. --- training-slides/src/closures.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/training-slides/src/closures.md b/training-slides/src/closures.md index b2fffe4..872988b 100644 --- a/training-slides/src/closures.md +++ b/training-slides/src/closures.md @@ -79,6 +79,8 @@ If you try and call `update()` after the `println!` you get a compile error. ## Capturing by transferring ownership +This closure implements `FnOnce`. + ```rust fn main() { let items = vec![1, 2, 3, 4];