Rust from Basics to Mastery — Part 9
Configuring concurrency with threads, channels, and mutexes
The articles written on this blog were created with the goal of teaching. Also, the information in the article may be incorrect, so please be careful.
Rust restricts shared access to types, which can create data races. Even if you use threads, channels, or mutexes, you must design task termination and lock scope yourself.
Starting and ending threads
use std::thread;
let handle = thread::spawn(|| {
load_world_data()
});
let world = handle
.join()
.expect("worker thread panicked");
The closure passed to spawn is executed in a new thread. join waits for task completion and returns whether or not there is a panic as a result.
If a closure uses an outer value, it must outlive the thread, so ownership is often transferred to move.
Send message to channel
use std::sync::mpsc;
let (sender, receiver) = mpsc::channel();
thread::spawn(move || {
sender.send(String::from("Loaded")).unwrap();
});
if let Ok(message) = receiver.recv() {
println!("{message}");
}
Channels move messages from one task to another rather than sharing values. When all senders disappear, the receiver knows that the channel is closed.
Arc and Mutex
use std::sync::{Arc, Mutex};
let score = Arc::new(Mutex::new(0));
let worker_score = Arc::clone(&score);
let handle = thread::spawn(move || {
let mut value = worker_score.lock().unwrap();
*value += 10;
});
handle.join().unwrap();
Arc shares ownership between threads, and Mutex allows only one change access at a time. Holding a lock for a long time or acquiring multiple locks in a different order can lead to poor performance and deadlocks.
Concurrency is not about increasing the number of threads. After reducing shared state and defining message boundaries, you need to actually measure the size of the operation and latency.