Concurrency and Multithreading in Rust: From A to Z

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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::spawn takes a closure that executes in a new thread.
  • The join method 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                

Blogs

Book Recommendations