- 📘 Day 17 - Concurrency in Rust
Welcome to Day 17 of the 30 Days of Rust Challenge! 🎉 Today, we embark on an exciting journey into Concurrency in Rust. Rust’s fearless concurrency model enables developers to write robust and safe multithreaded applications. 🚀
Join the 30 Days of Rust community on Discord for discussions, questions, and to share your learning journey! 🚀
Concurrency is the ability to execute multiple tasks simultaneously or interleave their execution to improve responsiveness and performance.
In this lesson, we will learn about:
- Rust's traits for concurrency:
Send
andSync
. - Using threads and message passing.
- Synchronizing shared state with
Mutex
. - Preventing and debugging deadlocks.
- Advanced concurrency concepts like
Arc
,Rayon
, and when to useCell
,RefCell
, orMutex
.
If you have already set up your Rust environment on Day 1, you’re good to go! Otherwise, check out the Environment Setup section for detailed instructions. Ensure you have Cargo installed by running:
$ cargo --version
If you see a version number, you’re all set! 🎉
Concurrency is about enabling multiple tasks to progress concurrently. Rust provides:
- Fearless concurrency: Ensures safety through its ownership and type system.
- Memory safety: Prevents data races at compile time.
- Customizability: Offers primitives like threads, channels,
Mutex
, and community crates likeRayon
.
Concurrency ≠ Parallelism:
- Concurrency: Multiple tasks interleaved.
- Parallelism: Tasks executed simultaneously on multiple processors/cores.
Rust ensures concurrency safety through two marker traits:
-
Send
: Allows transferring ownership between threads.- Types like
u8
,String
, andVec
areSend
. - Non-thread-safe types like
Rc
are notSend
.
- Types like
-
Sync
: Allows shared access across threads via immutable references.- Types without interior mutability (e.g.,
i32
) areSync
. Arc
isSync
if its contents areSync
.
- Types without interior mutability (e.g.,
Rust’s std::thread
module provides an easy way to create and manage threads.
Example: Creating a thread:
use std::thread;
fn main() {
let handle = thread::spawn(|| {
for i in 1..5 {
println!("Hi from thread: {}", i);
}
});
for i in 1..5 {
println!("Hi from main: {}", i);
}
handle.join().unwrap();
}
Output:
Hi from main: 1
Hi from thread: 1
Hi from main: 2
Hi from thread: 2
...
thread::spawn
: Spawns a new thread to execute a closure.handle.join()
: Waits for the thread to finish.
Rust's std::thread
module allows spawning threads easily.
use std::thread;
fn main() {
let handle = thread::spawn(|| {
println!("Hello from a thread!");
});
handle.join().unwrap(); // Wait for the thread to finish
}
To move data into threads:
let data = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("{:?}", data);
});
handle.join().unwrap();
Rust uses message passing to enable threads to communicate safely. This is implemented using channels from the std::sync::mpsc
module (multiple producer, single consumer).
Rust's mpsc
(multiple producer, single consumer) channels allow safe message passing.
Example: Sending messages between threads:
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
tx.send("Hello from the thread!").unwrap();
});
let received = rx.recv().unwrap();
println!("{}", received);
}
Output:
Hello from the thread!
To share data between threads, Rust provides Mutex (mutual exclusion). A Mutex
ensures that only one thread accesses data at a time.
Mutex<T>
provides mutual exclusion for shared mutable data. Combine it with Arc
for multithreading:
Example: Using Mutex
:
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());
}
Output:
Result: 10
A deadlock occurs when two or more threads wait indefinitely for each other to release a resource.
Deadlocks occur when threads block each other indefinitely.
Avoid deadlocks by:
- Locking resources in a consistent order.
- Using non-blocking algorithms.
- Leveraging message passing over shared state.
- Use timeouts for locking.
- Prefer message-passing or atomics for simpler cases.
Example of deadlock (avoid this pattern!):
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let lock1 = Arc::new(Mutex::new(()));
let lock2 = Arc::new(Mutex::new(()));
let lock1_clone = Arc::clone(&lock1);
let lock2_clone = Arc::clone(&lock2);
let thread1 = thread::spawn(move || {
let _ = lock1_clone.lock().unwrap();
let _ = lock2_clone.lock().unwrap();
});
let thread2 = thread::spawn(move || {
let _ = lock2.lock().unwrap();
let _ = lock1.lock().unwrap();
});
thread1.join().unwrap();
thread2.join().unwrap();
}
Arc<T>
allows shared ownership of immutable data across threads. Combine it with Mutex<T>
for mutability.
Rayon
provides easy parallelism with iterators:
use rayon::prelude::*;
fn main() {
let data = vec![1, 2, 3, 4, 5];
let result: Vec<_> = data.par_iter().map(|x| x * 2).collect();
println!("{:?}", result);
}
Type | Use Case | Thread-Safe | Notes |
---|---|---|---|
Cell |
Single-threaded, interior mutability | ❌ | For simple cases |
RefCell |
Single-threaded runtime checks | ❌ | Panics on multiple borrows |
Mutex |
Multithreaded, shared state | ✅ | Thread-safe with locking |
- Create a program that spawns multiple threads to increment a shared counter. Use
Arc
andMutex
to protect the shared counter. - Extend the program to print the value of the counter after each increment.
Example Code:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let handles: Vec<_> = (0..5)
.map(|_| {
let counter = Arc::clone(&counter);
thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
println!("Counter: {}", *num);
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
}
- Threads: Create a program that spawns 10 threads, each printing its ID.
- Shared State: Implement a thread-safe counter with
Arc
andMutex
. - Message Passing: Write a producer-consumer model with
mpsc
. - Parallelism: Use
Rayon
to parallelize a computationally heavy task.
Write a program to simulate a producer-consumer system using Rust's mpsc
channel. The producer generates random numbers, and the consumer calculates their sum.
Use the Rayon
library to parallelize a computation-heavy task, such as calculating the sum of squares for a large range of numbers.
Write a program that intentionally creates a deadlock by locking multiple Mutex
objects in different orders. Extend the program to resolve the deadlock by reordering the locks.
Create a simple logging system that writes log entries to a file with timestamps. Categorize logs as INFO
, WARNING
, and ERROR
.
-
Implement a program to:
- Create a file named
example.txt
. - Write "Concurrency is fun in Rust!" into the file.
- Read the file content and print it to the console.
- Create a file named
-
Use threads to print numbers from 1 to 5 in parallel.
-
Create a thread-safe counter that increments in multiple threads and prints its final value.
-
Reverse Message Passing: Modify the producer-consumer program to send data in reverse order (consumer sends back processed results to the producer).
-
File-based Thread Communication: Write two programs:
- A writer program that writes data to a file.
- A reader program that reads and processes the data from the same file.
-
Build a File Copier with Threads: Implement a program that reads from one file and writes to another file in parallel threads.
-
Deadlock Debugger: Write a program that detects and logs deadlocks between threads to a file.
Today, we explored Rust's approach to concurrency:
- Created and managed threads with
std::thread
. - Used channels for safe message passing.
- Implemented shared state with
Mutex
. - Learned to identify and avoid deadlocks.
Rust's ownership model ensures safe and efficient concurrency. Practice these concepts and experiment with concurrent programs to solidify your understanding.
Stay tuned for Day 18, where we will explore Asynchronous Programming in Rust in Rust! 🚀
🌟 Great job on completing Day 17! Keep practicing, and get ready for Day 18!
Thank you for joining Day 17 of the 30 Days of Rust challenge! If you found this helpful, don’t forget to star this repository, share it with your friends, and stay tuned for more exciting lessons ahead!
Stay Connected
📧 Email: Hunterdii
🐦 Twitter: @HetPate94938685
🌐 Website: Working On It(Temporary)