Concurrency and Multithreading in Rust: Writing Safe and Fast Code
Modern programs rarely run in single-threaded mode. Request handling, file operations, network tasks — all of this requires parallel execution of tasks. Rust offers a unique approach to concurrency: it guarantees memory safety at compile time, preventing data races. In this article, we will explore how to work with threads, channels, mutexes, and asynchrony in Rust.
1. Basics of Multithreading: Creating and Managing Threads
In Rust, threads are created using the standard library std::thread. Each thread executes independently, but the compiler strictly monitors variable lifetimes.
Creating a Thread
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(100)); } });
for i in 1..5 { println!("Main thread: {}", i); thread::sleep(Duration::from_millis(100)); }
handle.join().unwrap(); // Wait for the thread to finish}Capturing Ownership in Closures
If a thread uses variables from an outer scope, ownership must be transferred using move:
let data = vec![1, 2, 3];
let handle = thread::spawn(move || { println!("Data: {:?}", data);});
handle.join().unwrap();// println!("{:?}", data); // Error! data has been moved2. Data Sharing: Mutexes and Locks
For safe access to shared data from multiple threads, Rust uses Mutex in conjunction with Arc (atomic reference counter).
Example with Mutex and Arc
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: Mutex is automatically unlocked when it goes out of scope. This prevents deadlocks at the compiler level.
RwLock — for Frequent Reads
If data is rarely modified but frequently read, use RwLock:
use std::sync::RwLock;
let lock = RwLock::new(5);
{ let r1 = lock.read().unwrap(); let r2 = lock.read().unwrap(); // Can read simultaneously println!("{} {}", r1, r2);}
{ let mut w = lock.write().unwrap(); *w += 1; // Writing blocks reading}3. Channels: Message Passing Between Threads
Rust supports the actor model through channels (std::sync::mpsc). This allows threads to exchange data without shared memory.
Example with a Channel
use std::sync::mpsc;use std::thread;
fn main() { let (tx, rx) = mpsc::channel();
thread::spawn(move || { let vals = vec!["hello", "from", "the", "thread"]; for val in vals { tx.send(val).unwrap(); thread::sleep(std::time::Duration::from_secs(1)); } });
for received in rx { println!("Received: {}", received); }}Multiple Senders
Using mpsc::Sender, you can clone the sender for multiple threads:
let (tx, rx) = mpsc::channel();let tx1 = tx.clone();
thread::spawn(move || { tx1.send("Thread 1").unwrap(); });thread::spawn(move || { tx.send("