Rust threading: Difference between revisions

From wikinotes
 
(3 intermediate revisions by the same user not shown)
Line 21: Line 21:


fn main() {
fn main() {
     let handle = thread::spawn(|| {
     let handle = thread::spawn(|| {                 // <<-- note closure without 'move'
         for i in 1..=5 {
         for i in 1..=5 {
             println!("step {}/5..", i);
             println!("step {}/5..", i);
Line 38: Line 38:
fn main() {
fn main() {
     let name = String::from("alex");
     let name = String::from("alex");
     let handle = thread::spawn(move || {
     let handle = thread::spawn(move || {         // <-- 'move' changes thread affinity of closure vars
         for _ in 1..=5 {
         for _ in 1..=5 {
             println!("hi {}!", name);
             println!("hi {}!", name);
Line 115: Line 115:
</blockquote><!-- mutexes -->
</blockquote><!-- mutexes -->
</blockquote><!-- Synchronization -->
</blockquote><!-- Synchronization -->
= Ownership =
<blockquote>
== Traits ==
<blockquote>
Automatically Applied:
* <code>Send</code> implementors can have their ownership transferred between threads
* <code>Sync</code> implementors indicate they can safely be referenced by multiple threads
Manually Applied
* <code>Arc<T></code> trait implementors allow reference-counting between threads (see [[rust pointers]])
</blockquote><!-- Traits -->
</blockquote><!-- Ownership -->

Latest revision as of 04:19, 11 February 2023

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)