Rust threading

From wikinotes

Documentation

rust book: concurrency https://doc.rust-lang.org/stable/book/ch16-00-concurrency.html
std::sync builtin synchronization primitives

Basics

Since rust already manages ownership semantics, you don't really need to deal with thread affinity.
Simply pass a closure to a thread, move any params to it, and call it a day.

thead without outer scope access

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {                 // <<-- note closure without 'move'
        for i in 1..=5 {
            println!("step {}/5..", i);
            thread::sleep(Duration::from_secs(1));
        }
    });
    handle.join().expect("unable to join thread");
}

thread that moves outer-scope into thread's closure

use std::thread;
use std::time::Duration;

fn main() {
    let name = String::from("alex");
    let handle = thread::spawn(move || {          // <-- 'move' changes thread affinity of closure vars
        for _ in 1..=5 {
            println!("hi {}!", name);
            thread::sleep(Duration::from_secs(1));
        }
    });
    handle.join().expect("unable to join thread");
}

Synchronization

mspc::channel

multiple producer, single consumer, FIFO queue.
do work in threads, have eventloop in main thread.

use std::thread;
use std::sync::mpsc;

fn main() {
    let (tx, rx) = mpsc::channel();

    let handle = thread::spawn(move|| {
        tx.send(123).unwrap();
    });

    println!("hello to {}", rx.recv().unwrap());
    handle.join().expect("unable to join thread");
}

mutexes

rust mutexes are a bit unique, in that they wrap the object you are protecting.
locking the mutex provides access to the object,
and when it falls out of scope, the lock is released.

let m = Mutex::new(5);
{
    let mut num = m.lock().unwrap();
    *num = 6;  // mutate the protected value
} // lock is released

If a mutex is shared between threads, wrap it in Arc<T> so the reference is not deleted.
This behaves like a Rc<T> for threads (see rust pointers for more details).

use std::thread;
use std::sync::Arc;
use std::sync::Mutex;

fn main() {
    let counter_lock_arc = Arc::new(Mutex::new(0));
    let counter_lock = Arc::clone(&counter_lock_arc);  // clone before we move ownership to closure

    let spawn_thread = || {
        let counter_lock = Arc::clone(&counter_lock_arc);
        thread::spawn(move|| {
            let mut count = counter_lock.lock().unwrap();
            *count += 1;
        })
    };

    (1..=3)
        .map(|_| spawn_thread())
        .for_each(|h| h.join().expect("unable to join thread"));

    println!("count: {}", counter_lock.lock().unwrap());
}

Ownership

Traits

Automatically Applied:

  • Send implementors can have their ownership transferred between threads
  • Sync implementors indicate they can safely be referenced by multiple threads

Manually Applied

  • Arc<T> trait implementors allow reference-counting between threads (see rust pointers)