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
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
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 looks like this:
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
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.
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...
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.
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...
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...
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.
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); }