Concurrency and Multithreading in Rust: From A to Z
Modern applications increasingly require efficient use of multi-core processors. Rust offers a unique approach to concurrency and multithreading, combining high performance with memory safety guarantees at compile time. In this article, we will cover key concepts: from creating threads to using mutexes and channels, with practical code examples.
1. Basics of Multithreading: Creating and Managing Threads
In Rust, threads are created using the standard library std::thread. Each thread executes independently but can exchange data with other threads.
A simple example of creating a thread:
use std::thread;use std::time::Duration;
fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!("Thread: number {}", i); thread::sleep(Duration::from_millis(1)); } });
for i in 1..5 { println!("Main thread: number {}", i); thread::sleep(Duration::from_millis(1)); }
handle.join().unwrap(); // Wait for the thread to finish}Key points:
thread::spawntakes a closure that executes in a new thread.- The
joinmethod blocks the current thread until the specified thread completes. - Without
join, the main thread may finish before the child thread.
To pass data between threads, use closures with move:
use std::thread;
fn main() { let data = vec![1, 2, 3, 4, 5];
let handle = thread::spawn(move || { println!("Data from thread: {:?}", data); });
handle.join().unwrap(); // println!("{:?}", data); // Error! data has been moved}The move keyword transfers ownership of data into the closure, preventing data races at compile time.
2. Safe Data Sharing: Mutexes and Locks
When multiple threads access shared data, there is a risk of data races. Rust solves this problem using mutexes (Mutex) and atomic types.
Example of using Mutex 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()); // 10}Important details:
Arc<T>allows multiple threads to own the same data (atomic cloning).Mutex::lock()returns a lock; if the lock is already held, the thread waits.- The lock is automatically released when it goes out of scope.
For finer control, use RwLock, which separates readers and writers:
use std::sync::RwLock;
let lock = RwLock::new(5);
// Reading: multiple threads simultaneously{ let r1 = lock.read().unwrap(); let r2 = lock.read().unwrap(); println!("Reading: {} and {}", r1, r2);}
// Writing: only one thread{ let mut w = lock.write().unwrap(); *w += 1; println!("Wrote: {}", w);}3. Channels for Message Passing
An alternative to shared memory is the actor model using channels. Rust provides a multi-producer, single-consumer channel (mpsc).
Basic example:
u