Concurrency and Multithreading in Rust: Writing Safe Parallel Code
Rust is one of the few languages that solves the problem of data races at compile time. Thanks to the ownership and borrowing system, concurrent code in Rust becomes not only fast but also safe. In this article, we will cover the basics of multithreading in Rust: creating threads, exchanging data through channels and mutexes, and also get acquainted with the "Fearless Concurrency" model.
1. Basics of Multithreading: std::thread
Rust provides built-in support for threads through the std::thread module. You can create a new thread using the thread::spawn function, which takes a closure.
use std::thread;use std::time::Duration;
fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!("Thread: {}", i); thread::sleep(Duration::from_millis(1)); } });
for i in 1..5 { println!("Main thread: {}", i); thread::sleep(Duration::from_millis(1)); }
handle.join().unwrap(); // wait for the thread to finish}The join() method blocks the main thread until the child thread completes. Without it, the program may finish before the thread executes.
2. Data Transfer Between Threads: Channels (mpsc)
For message passing between threads, Rust uses the multiple producer, single consumer channel model (mpsc). This is implemented in the std::sync::mpsc module.
use std::sync::mpsc;use std::thread;
fn main() { let (tx, rx) = mpsc::channel();
thread::spawn(move || { let val = String::from("Hello from thread!"); tx.send(val).unwrap(); // val has already been moved, cannot use it anymore });
let received = rx.recv().unwrap(); println!("Received: {}", received);}Note the move keyword in the closure — it transfers ownership of the tx variable to the new thread. The send() method returns a Result, which can be Err if the receiver has been closed.
To create multiple producers, you can clone tx:
let (tx, rx) = mpsc::channel();let tx1 = tx.clone();
thread::spawn(move || { tx.send("Message 1").unwrap();});
thread::spawn(move || { tx1.send("Message 2").unwrap();});
for received in rx { println!("Received: {}", received);}3. Shared Memory: Mutex and Arc
Channels are convenient for data transfer, but sometimes you need to share state between threads. For this, Mutex (mutual exclusion) is used in conjunction with Arc (atomic reference counting).
use std::sync::{Arc, Mutex};use std::thread;
fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![];
for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); }
for handle in handles { handle.join().unwrap(); }
println!("Result: {}", *counter.lock().unwrap());}Why Arc and not Rc? Regular Rc is not thread-safe because its reference counter is not atomic. Arc uses atomic operations, making it safe for multithreading.
Important: Mutex blocks the thread when lock() is called. After going out of scope, the lock is automatically released (thanks to the Drop trait).