Skip to content

Commit

Permalink
Merge pull request #68 from ferrous-systems/spawning-threads
Browse files Browse the repository at this point in the history
Add threads and closures.
  • Loading branch information
jonathanpallant authored Jun 14, 2023
2 parents e0dcf03 + 8fff621 commit 1d80def
Show file tree
Hide file tree
Showing 2 changed files with 334 additions and 0 deletions.
152 changes: 152 additions & 0 deletions training-slides/src/closures.md
Original file line number Diff line number Diff line change
@@ -1 +1,153 @@
# Closures

## Rust's Function Traits

* `trait FnOnce<Args>`
* `trait FnMut<Args>: FnOnce<Args>`
* `trait Fn<Args>: FnMut<Args>`

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 `|<args>|`
* 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

This closure implements `FnOnce`.

```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, T>(f: F) -> T where F: FnOnce(&mut Vec<u32>) -> 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, T>(f: F) -> T where F: FnOnce(&mut Vec<u32>) -> 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.
182 changes: 182 additions & 0 deletions training-slides/src/spawning-threads.md
Original file line number Diff line number Diff line change
@@ -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, T>(f: F) -> JoinHandle<T>
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<Vec<i32>> = 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);
}
```

0 comments on commit 1d80def

Please sign in to comment.