We discuss more advanced part of the Rust programming language: the concurrency. Already covered contents will not be discussed in this note.
A Mutex
in Rust gets marked as poisoned when a thread panics while holding the lock because the panicked thread will hold the lock forever. When that happens, the Mutex
will no longer be locked, but calling its lock
method will result in an Err
to indicate it has been poisoned. However, Err
returned from poisoned mutexes are often disregarded and the calling process will simply panic to prevent errors from propagating to more processes.
MutexGuard
's LifetimeA well-designed Mutex will typically implement the Drop
trait as follows.
impl<'a, S, T> Drop for MutexGuard<'a, S, T>
where S: LockStrategy,
{
fn drop(&mut self) {
self.mutex.unlock();
}
}
So it means we can always assume that the lock will be dropped until if ends it lifetime, but sometimes, we may encounter some subtle problems due to lifetime mess-ups. For example,
vec.lock().push(123); // Ends after this statement. OK.
{
let _ = vec.lock(); // Will not deadlock and dropped at the end of this scope.
}
if let Some(item) = vec.lock().front() {
// A `let` statement will extend the lifetime until the while statement ends.
vec.lock().remove(0); // Deadlock.
}
Because the above syntax sugar will be expanded to the following.
match vec.lock().front() {
Some(item) => vec.lock().remove(0),
_ => (),
}
So the lifetime ends after match
is done. Therefore, a good coding habit for handling locks would be to always acquire a lock explicitly and drops it when necessary or wrapping the lock in a scope.
When data is mutated by multiple threads, there are many situations where they would need to wait for some event, for some condition about the data to become true. This is usually called conditional variable or CondVar
. There is a wonderful crate called parking_lot
that provides this functionality. While a mutex does allow threads to wait until it becomes unlocked, it does not provide functionality for waiting for any other conditions.
A thread can park itself if the condition is not satified and sleeps until other threads that work with the conditional variable notify the parked thread and wakes it up. The below examples illustrate the philosophy (although over-simplified):
use std::collections::VecDeque;
use std::time::Duration;
use std::sync::Mutex;
use std::thread;
fn main() {
let queue = Mutex::new(VecDeque::new());
thread::scope(|s| {
// Consuming thread
let t = s.spawn(|| loop {
let item = queue.lock().unwrap().pop_front();
if let Some(item) = item {
dbg!(item);
} else {
thread::park();
}
});
// Producing thread
for i in 0.. {
queue.lock().unwrap().push_back(i);
t.thread().unpark();
thread::sleep(Duration::from_secs(1));
}
});
}
This is ineffective because
push_back
.However, these problems can be easily tackled with by adding waiting queue (CondVar
) and a Mutex lock.