What is Concurrency in Rust?

Concurrency in Rust means running multiple tasks at the same time in a coordinated way, not necessarily exactly at the same moment.

Imagine, a chef preparing multiple dishes,:

  • While the soup is boiling, the chef chops vegetables.
  • While the oven preheats, the chef prepares the next dish.

The chef is not doing everything simultaneously, but tasks happen together in progress; that’s concurrency.

Rust provides safe and powerful concurrency tools like threads, channels, async/await, and more.

Its biggest strength is that it prevents common bugs (like data races, deadlocks, or memory corruption) at compile time, meaning many concurrency errors are caught before your program even runs.

Concurrency Basics in Rust

Rust provides a built-in std::thread module for working with threads. You can create and manage threads easily with Rust’s standard library.

A thread is like a mini-program running inside your main program. It runs separately but can work together with other threads.

Example: Creating a Thread

use std::thread;

fn main() {
// Spawn a new thread
let worker = thread::spawn(|| {
for i in 1..=4 {
println!("Worker thread says: {}", i);
}
});

// Main thread runs in parallel
for i in 1..=4 {
println!("Main thread says: {}", i);
}

// Wait for the worker thread to finish before exiting
worker.join().unwrap();
}

Explanation:

  • The thread::spawn function creates a new thread.
  • join ensures the main thread waits for the spawned thread to finish.

Sharing Data Between Threads

When you run multiple threads in a program, they need to access and modify the same data. But this can be dangerous, if two threads try to change the same variable at the same time, it causes a race condition (unpredictable and unsafe behavior).

Rust solves this problem safely using special tools like Mutex, Arc, and Channels.

How To Use Mutex and Arc Together?

A Mutex (Mutual Exclusion) allows only one thread at a time to access the data. On the other hand, Arc allows multiple threads to share ownership of the same data safely.

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
// Shared counter inside a Mutex, wrapped with Arc for safe sharing
let counter = Arc::new(Mutex::new(0));
let mut threads = vec![];

// Create 5 threads that will all increase the same counter
for _ in 0..5 {
let shared_counter = Arc::clone(&counter); // Clone Arc to share ownership
let handle = thread::spawn(move || {
// Lock the Mutex to get safe, exclusive access
let mut num = shared_counter.lock().unwrap();
*num += 1;
println!("Thread increased counter to: {}", *num);
});
threads.push(handle);
}

// Wait for all threads to finish
for handle in threads {
handle.join().unwrap();
}

// Safely read the final value
let final_value = *counter.lock().unwrap();
println!("Final counter value: {}", final_value);
}

Explanation:

  • Mutex::new(0) → Creates a Mutex with the initial value 0.
  • Arc::new(…) → Wraps it in an Arc so multiple threads can share ownership.
  • Arc::clone(&counter) → Makes a new reference for each thread (safe shared ownership).

Output Example:

Thread increased counter to: 1
Thread increased counter to: 2
Thread increased counter to: 3
Thread increased counter to: 4
Thread increased counter to: 5
Final counter value: 5

How To Use Channels for Communication?

When multiple threads are running, sometimes they need to send messages or data to each other. But sharing the same memory can create problems, so the safe and cleaner way is to use channels.

For example: Sending Messages Between Threads

use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
// Create a channel: tx = transmitter (sender), rx = receiver
let (tx, rx) = mpsc::channel();

// Spawn a new thread to send messages
thread::spawn(move || {
let messages = vec!["Rust", "makes", "thread", "communication", "easy"];
for msg in messages {
// Send each message through the channel
tx.send(msg).unwrap();
println!("Sent: {}", msg);
thread::sleep(Duration::from_millis(400)); // Simulate work
}
});

// Receive and print the messages in the main thread
for received in rx {
println!("Received: {}", received);
}

println!("All messages received!");
}

In this code:

  • mpsc::channel() creates a new communication channel.
    • tx is the transmitter (sender side)
    • rx is the receiver (receiver side)
  • The sender (tx) sends messages to the receiver (rx), ensuring safe communication between threads.

Avoiding Common Concurrency Pitfalls

Rust is designed to prevent many of these at compile time, but it’s still important to understand them. Here are two of the most common concurrency issues:

1) Data Races: A data race happens when, two or more threads access the same memory location at the same time. At least one thread writes/changes the data. And there’s no proper synchronization.

Rust completely prevents data races at compile time using ownership, borrowing, and synchronization tools like Mutex and Arc.

2) Deadlocks: A deadlock happens when two or more threads are waiting forever for each other to release a lock, and as a result, your program gets stuck.

For example:

  • Thread A locks Resource 1 and waits for Resource 2.
  • Thread B locks Resource 2 and waits for Resource 1.
  • Both thread can’t process and program freezes.

Rust cannot detect deadlocks at compile time because they are logical issues, not syntax errors.

Example: Deadlock Example

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
let wallet = Arc::new(Mutex::new(100));
let bank = Arc::new(Mutex::new(200));

let wallet_clone = Arc::clone(&wallet);
let bank_clone = Arc::clone(&bank);

// Thread 1 locks wallet first, then bank
let t1 = thread::spawn(move || {
let _lock_wallet = wallet_clone.lock().unwrap();
println!("Thread 1 locked wallet");
std::thread::sleep(std::time::Duration::from_millis(100)); // Simulate work
let _lock_bank = bank_clone.lock().unwrap();
println!("Thread 1 locked bank");
});

// Thread 2 locks bank first, then wallet
let t2 = thread::spawn(move || {
let _lock_bank = bank.lock().unwrap();
println!("Thread 2 locked bank");
std::thread::sleep(std::time::Duration::from_millis(100)); // Simulate work
let _lock_wallet = wallet.lock().unwrap();
println!("Thread 2 locked wallet");
});

t1.join().unwrap();
t2.join().unwrap();
}

How to Fix Deadlocks

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
let wallet = Arc::new(Mutex::new(100));
let bank = Arc::new(Mutex::new(200));

let wallet_clone = Arc::clone(&wallet);
let bank_clone = Arc::clone(&bank);

// Both threads lock in the same order: wallet -> bank
let t1 = thread::spawn(move || {
let _lock_wallet = wallet_clone.lock().unwrap();
println!("Thread 1 locked wallet");
let _lock_bank = bank_clone.lock().unwrap();
println!("Thread 1 locked bank");
});

let t2 = thread::spawn(move || {
let _lock_wallet = wallet.lock().unwrap();
println!("Thread 2 locked wallet");
let _lock_bank = bank.lock().unwrap();
println!("Thread 2 locked bank");
});

t1.join().unwrap();
t2.join().unwrap();
}
  • Result: Both threads follow the same lock order, so they never wait on each other forever. No deadlock occurs.

Concurrency in Asynchronous Programming

In Rust, asynchronous programming is another powerful way to achieve concurrency, but without using multiple threads directly.

Normally, when a function does something slow (like reading a file or calling an API), the program waits until it’s done. This blocks execution and wastes time.

With async programming, instead of waiting, the program can pause that task and do something else, then resume it when ready.

This means many tasks can make progress without blocking each other, even if they’re all running in one thread.

Example: Async Concurrency in Rust

use tokio::time::{sleep, Duration};
use tokio::task;

#[tokio::main]
async fn main() {
let task1 = task::spawn(async {
for i in 1..=3 {
println!("Downloading file chunk {}...", i);
sleep(Duration::from_millis(400)).await;
}
println!("File download complete!");
});

let task2 = task::spawn(async {
for i in 1..=3 {
println!("Saving data part {}...", i);
sleep(Duration::from_millis(300)).await;
}
println!("Data saved successfully!");
});

// Wait for both tasks to finish
task1.await.unwrap();
task2.await.unwrap();
}

Explanation:

  • Async tasks run concurrently without creating threads for each task.
  • Await Pauses execution until the asynchronous operation finishes.
  • task: :spawn: Runs asynchronous tasks concurrently (like small, lightweight workers).
  • tokio: :main: The entry point for asynchronous programs when using the Tokio runtime.

Learn More About Rust Programming

Leave a Comment