Run Code Simultaneously
- Rust provides built-in support for concurrent programming through its standard library module
std::thread
. - Threads in Rust allow multiple parts of a program to execute simultaneously, leveraging multi-core processors.
Threads
-
You can spawn threads using
std::thread::spawn
, which takes a closure, runs it in a new thread, and returnJoinHandle
. -
We can wait the spawned thread to finish using
join
method from the returnedJoinHandle
.pub fn join(self) -> Result<T>
: Waits for the associated thread to finish. This function will return immediately if the associated thread has already finished. -
We often use the
move
keyword with closures passed tothread::spawn
because the closure will then take ownership of the values it uses from the environment, thus transferring ownership of those values from one thread to another
Example
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
}
Using Smart Pointers, Mutex, and Channels in Threads
Shared Mutable State with Arc and Mutex
In a multi-threaded program, if we need shared mutable state, we can use:
Arc<T>
:- An atomic reference-counted smart pointer for shared ownership across threads.
Mutex<T>
:- A synchronization primitive for mutual exclusion to safely access shared data.
lock()
: Acquires a lock. Blocks the thread if a lock is held.try_lock
(): Attempts to acquire a lock immediately. If lock cant be acquired, return anErr
.
Example
use std::{
sync::{Arc, Mutex},
thread,
time::Duration,
};
struct JobStatus {
jobs_done: u32,
}
fn main() {
let status = Arc::new(Mutex::new(JobStatus { jobs_done: 0 }));
let mut handles = Vec::new();
for _ in 0..10 {
let status_shared = Arc::clone(&status);
let handle = thread::spawn(move || {
thread::sleep(Duration::from_millis(250));
// lock and update jobs_done
status_shared.lock().unwrap().jobs_done += 1
});
handles.push(handle);
}
// Waiting for all jobs to complete.
for handle in handles {
handle.join().unwrap();
}
println!("Jobs done: {}", status.lock().unwrap().jobs_done);
}
Arc<T>
: Enables shared ownership of theMutex
across threads.Mutex<T>
: Ensures only one thread accesses the counter at a time.
Multiple Readers Single Writer
`RwLock (short for Read-Write Lock) is a synchronization primitive that allows multiple readers or a single writer to access shared data. It ensures safe concurrent access in a multi-threaded environment.
- Multiple Readers: Multiple threads can hold a read lock simultaneously if no thread is holding the write lock.
- Single Writer: Only one thread can hold the write lock, and it has exclusive access to the data.
RwLock
is part of the std::sync
module and is frequently used when you want to share data among threads with both read and write operations while minimizing contention.
- Read Lock
read()
: Acquires a read lock. Blocks the thread if a write lock is held. Returns an immutable reference to the data.try_read()
: Attempts to acquire a read lock immediately and returnErr
if cannot acquire read lock.
- Write Lock
write()
: Acquires a write lock. Blocks the thread if any read or write lock is held. Returns a mutable reference to the data.try_write()
: Attempts to acquire a write lock immediately and returnErr
if cannot acquire write lock.
Example
use std::{
sync::{Arc, RwLock},
thread,
time::Duration,
};
struct JobStatus {
jobs_done: u32,
}
fn main() {
let status = Arc::new(RwLock::new(JobStatus { jobs_done: 0 }));
let mut handles = Vec::new();
// Write jobs
for _ in 0..10 {
let status_shared = Arc::clone(&status);
let handle = thread::spawn(move || {
thread::sleep(Duration::from_millis(250));
// write lock and update jobs_done
status_shared.write().unwrap().jobs_done += 1
});
handles.push(handle);
}
// Read jobs
for _ in 0..10 {
let status_shared = Arc::clone(&status);
let handle = thread::spawn(move || {
thread::sleep(Duration::from_millis(250));
// read lock and get jobs_done
println!("Jobs done: {}", status_shared.read().unwrap().jobs_done);
});
handles.push(handle);
}
// Waiting for all jobs to complete.
for handle in handles {
handle.join().unwrap();
}
}
- The counter is wrapped in
Arc<RwLock<T>>
so it can be safely shared between threads. Arc
provides reference counting for thread-safe ownership.write()
: Acquires a write lock, granting exclusive mutable access to the shared counter.read()
: Acquires a read lock, allowing multiple threads to access the counter immutably.- The locks ensure that data races are prevented when accessing the shared counter.
Communication Between Threads with Channels
Rust provides channels for thread communication in the std::sync::mpsc
module:
mpsc
: Stands for multiple producers, single consumer.- That means we can create multiple
Sender
usingclone
method.
Example
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel(); // Create a channel
// clone Sender
let txc = tx.clone();
// Spawn a thread to send messages
thread::spawn(move || {
let messages = vec![1, 2, 3, 4, 5];
for msg in messages {
txc.send(msg).unwrap(); // Send a message
thread::sleep(Duration::from_millis(100));
}
});
// Spawn a thread to send messages
thread::spawn(move || {
let messages = vec![6, 7, 8, 9, 10];
for msg in messages {
tx.send(msg).unwrap(); // Send a message
thread::sleep(Duration::from_millis(100));
}
});
// Receive messages in the main thread
for received in rx {
println!("Received: {}", received);
}
}
mpsc::channel
creates a transmitter (tx
) and a receiver (rx
).- Messages are sent via the Sender
tx
and received via the Receiverrx
.
Best Practices for Multithreading
- Minimize Lock Contention:
- Keep the critical section (where the mutex is locked) as short as possible to avoid slowing down threads.
- Use
Arc
andMutex
Wisely:- Avoid using shared state unless necessary.
- Prefer message passing with channels for better isolation.