Parallelism through Threads
How to create multiple independent actors? How do they get access to the board?
| Feature | Threads | Processes |
|---|---|---|
| Address space | Shared | Separate |
| Stacks | Separate | One per thread |
| Program Counter | Separate | One per thread |
| Creation cost | Cheap(-ish) | Expensive |
| Operation | POSIX | Windows |
|---|---|---|
| Creation | pthread_create |
CreateThread |
| Wait for completion | pthread_join |
WaitForSingleObject |
| Exiting | pthread_exit |
ExitThread |
| Suspend & Resume | Condition variables using pthread_cond_wait, pthread_cond_signal |
SuspendThread, ResumeThread, or condition variables |
std::thread in C++threading.Thread in Pythonstd::thread module in Rustfn main() {
let mut board = gen_board();
let split_point = 4;
let counting_region = &board[..split_point];
let scrambling_region = &mut board[split_point..];
// Both threads share the `board` resource, but who owns it?
// (WARNING: Doesn't compile!)
std::thread::spawn(|| count_whites(counting_region));
std::thread::spawn(|| scramble(scrambling_region));
}main!'static (i.e. as long as the program)move || { ... })std::thread::scope)Arc<T>)static))fn process(lut: &[u8]) { todo!() }
fn main() {
let lookup_table: Vec<u8> = build_lut(configuration);
// Lift to multiple ownership (tracked at runtime):
let multi_owner_lut = Arc::new(lookup_table);
const NUM_WORKERS: usize = 4;
for _ in 0..NUM_WORKERS {
// Clones the *handle*, not the LUT itself!
let lut = Arc::clone(&multi_owner_lut);
std::thread::spawn(move || process(lut.as_ref()));
}
}Arc<T>: Atomic Reference Counted)Arc<T> in practiceArc<T> in practice (cont.)fn process(lut: &Lut, thread_id: u8) {
println!("{}", lut.lookup(thread_id));
}
fn main() {
// Lazily calculated LUT
let shared_lut = Arc::new(Lut::default());
const NUM_WORKERS: usize = 4;
for id in 0..NUM_WORKERS {
let lut = Arc::clone(&shared_lut);
std::thread::spawn(move || process(lut.as_ref(), id as u8));
}
}Arc<T> in practice (cont.)fn process(lut: &Lut, thread_id: u8) {
println!("{}", lut.lookup(thread_id));
}
fn main() {
// Lazily calculated LUT
let shared_lut = Arc::new(Lut::default());
const NUM_WORKERS: usize = 4;
for id in 0..NUM_WORKERS {
let lut = Arc::clone(&shared_lut);
std::thread::spawn(move || process(lut.as_ref(), id as u8));
}
}cannot borrow `*lut` as mutable, as it is behind a `&` reference
Arc<T> in practice (cont.)as_ref with as_mut:fn process(lut: &mut Lut, thread_id: u8) {
println!("{}", lut.lookup(thread_id));
}
fn main() {
// Lazily calculated LUT
let shared_lut = Arc::new(Lut::default());
const NUM_WORKERS: usize = 4;
for id in 0..NUM_WORKERS {
let lut = Arc::clone(&shared_lut);
std::thread::spawn(move || process(lut.as_mut(), id as u8));
}
}no method named `as_mut` found for struct `Arc<Lut>` in the current scope
Arc<T>Arc::get_mut, but it looks like this:None if this has more than one owner (i.e. the reference count is > 1)Shared references in Rust disallow mutation by default, and Arc is no exception: you cannot generally obtain a mutable reference to something inside an Arc.
Arc<T>, Rc<T>) only allow &T access&mut T from a &Arc<T> (or &Rc<T>)Arc<T> tracks the number of owners at runtime, we can track the number of borrows at runtimeCell<T>, RefCell<T> for single-threaded codeMutex<T>, RwLock<T>, Atomic<T> for multi-threaded code&Thing, all member variables of Thing are also immutable for me
&Thing, I can still mutate the member variables of Thing
Cell<T>use std::cell::Cell;
struct SomeStruct {
regular_field: u8,
special_field: Cell<u8>,
}
let my_struct = SomeStruct {
regular_field: 0,
special_field: Cell::new(1),
};
let new_value = 100;
// ERROR: `my_struct` is immutable
// my_struct.regular_field = new_value;
// WORKS: although `my_struct` is immutable, `special_field` is a `Cell`,
// which can always be mutated
my_struct.special_field.set(new_value);
assert_eq!(my_struct.special_field.get(), new_value); // get() returns *copy*use std::cell::RefCell;
struct SomeStruct {
regular_field: u8,
special_field: RefCell<u8>,
}
let my_struct = SomeStruct {
regular_field: 0,
special_field: RefCell::new(1),
};
let mut borrow = my_struct.special_field.borrow_mut();
*borrow = 100;
// Calling .borrow() again would panic!
// my_struct.special_field.borrow()RefCell<T> looks like magic!”RefCell<T> in multi-threaded codefn process(lut: Arc<RefCell<Lut>>, thread_id: u8) {
let mut borrow = lut.borrow_mut();
println!("{}", borrow.lookup(thread_id));
}
fn main() {
// Lazily calculated LUT
let shared_lut = Arc::new(RefCell::new(Lut::default()));
const NUM_WORKERS: usize = 4;
for id in 0..NUM_WORKERS {
let lut = Arc::clone(&shared_lut);
std::thread::spawn(move || process(lut, id as u8));
}
}RefCell<T> in multi-threaded codefn process(lut: Arc<RefCell<Lut>>, thread_id: u8) {
let mut borrow = lut.borrow_mut();
println!("{}", borrow.lookup(thread_id));
}
fn main() {
// Lazily calculated LUT
let shared_lut = Arc::new(RefCell::new(Lut::default()));
const NUM_WORKERS: usize = 4;
for id in 0..NUM_WORKERS {
let lut = Arc::clone(&shared_lut);
std::thread::spawn(move || process(lut, id as u8));
}
}`RefCell<Lut>` cannot be shared between threads safely
the trait `Sync` is not implemented for `RefCell<Lut>`
if you want to do aliasing and mutation between multiple threads, use `std::sync::RwLock` instead
RwLock<T> (or Mutex<T>) will solve our problem
Arc<RwLock<T>> or Arc<Mutex<T>> is a common Rust pattern for shared mutable ownershipArc<T> shares an instance between threadsMutex<T>/RwLock<T> prevent race conditionsSend and SyncSend and SyncSend if it is safe to send it to another threadSync if it is safe to share between threadsT is Sync if and only if &T is SendSend/Sync
UnsafeCell<T> (explicitly allows shared mutable state)Rc<T> (uses raw pointers internally)Send and Sync as generic boundsstd::thread::spawn again:Arc<T> is only Send if T is Sync
Arcs point to the same value, sending one Arc to another thread gives that thread access to the underlying value, so the value must be Sync!fn process(lut: Arc<Mutex<Lut>>, thread_id: u8) {
let mut lut = lut.lock().expect("Lock was poisoned");
println!("{}", lut.lookup(thread_id));
}
fn main() {
// Lazily calculated LUT
let shared_lut = Arc::new(Mutex::new(Lut::default()));
const NUM_WORKERS: usize = 4;
for id in 0..NUM_WORKERS {
let lut = Arc::clone(&shared_lut);
std::thread::spawn(move || process(lut, id as u8));
}
}These are all the rules we need to know for writing thread-safe code in Rust!