MutexGuard
in C++Rust has a nifty thing called a MutexGuard
,
which is similar to a std::unique_lock
in C++,
but also protects accesses of the underlying data at the same time.
Combined with the Rust borrow checker,
this basically enforces the common practice of
protecting memory with a mutex by only accessing it when it is locked.
MutexGuard
If you're already good with this, feel free to skip to the next section.
use std::ops::AddAssign;
use std::sync::Mutex;
fn main() {
let x = Mutex::<i32>::new(0); // : Mutex<i32>
{
let mut x_handle = x.lock().unwrap(); // : MutexGuard<i32>
// MutexGuard
implements Deref
and DerefMut
.
// In this case, we're using DerefMut
to modify the i32
contained within.
*x_handle = 1;
// Here, the handle is Drop
'd (Rust-speak for destructed).
// This unlocks the mutex automatically.
}
// For simple operations, we can just keep the MutexGuard
as a temporary.
// There are complex rules for how long the temporary stays around:
// https://doc.rust-lang.org/reference/destructors.html#drop-scopes
// But you don't have to worry about it in most cases.
// Generally, temporaries are destructed quickly, unless a borrow occurs.
// .
in Rust implicitly does Deref
/DerefMut
, so this is allowed.
x.lock().unwrap().add_assign(1);
// Scopes are still necessary when you want to do multiple things in one transaction.
{
let mut x_handle = x.lock().unwrap(); // : MutexGuard<i32>
// Here, the lock is held throughout the print, and the subsequent increment.
std::println!("{}", *x_handle);
*x_handle += 1;
{
// We can use try_lock
as well, which may or may not give us a MutexGuard
.
// This try_lock
fails because x_handle
is still alive.
let another_handle = x.try_lock(); // : Result<MutexGuard<i32>, TryLockError<…>>
match another_handle {
Ok(mut handle) => *handle += 1, // : MutexGuard<i32>
_ => {}
}
}
}
std::println!("{}", *x.lock().unwrap());
}
// Output:
// 2
// 3
MutexGuard
in C++Since C++ is the best language*,
it is unthinkable that Rust has such a good feature that C++ does not.
Let's implement it**!
* /s
** Well, minus the borrow checker part. So while slightly harder to misuse, it can still be abused.
First, let's work through what we want to be able to do with the MutexGuard
.
Porting the Rust code to C++, we get:
#include <iostream>
#include "mutexguard.hpp"
int main() {
Mutex<int> x{0};
{
auto x_handle = x.lock();
*x_handle = 1;
}
*x.lock() += 1;
{
auto x_handle = x.lock();
std::cout << *x_handle << std::endl;
*x_handle += 1;
if (auto another_handle = x.try_lock(); another_handle) {
**another_handle += 1;
}
}
std::cout << *x.lock() << std::endl;
}
// Expected output:
// 2
// 3
Now we just have to implemnet the functionality. We can see that we need:
Mutex
should contain an int
and a std::mutex
Mutex::lock()
should return a MutexGuard
handleMutexGuard::operator*()
should let us reference the int&
MutexGuard::operator->()
MutexGuard
should act like a std::unique_lock
Let's go ahead and implement it!
#include <mutex>
#include <optional>
template <typename ValueT, typename MutexT = std::mutex>
class Mutex {
// We store the data right beside the mutex *privately*.
// We ask the user to only access this data through the
// MutexGuard
handle returned by lock()
or try_lock().
ValueT val;
MutexT mut;
public:
class MutexGuard;
// We construct a new MutexGuard
by passing in a ref to val
and a lock
.
MutexGuard lock() { return MutexGuard{val, mut}; }
class MutexGuard {
ValueT& ref;
// By simply embedding a unique_lock
we don't need a destructor.
std::unique_lock<MutexT> lock;
public:
template <typename Lock>
MutexGuard(ValueT& ref, Lock&& lock)
: ref(ref), lock(std::forward<Lock>(lock)) {}
// The equivalent of DerefMut
.
ValueT& operator*() { return ref; };
ValueT* operator->() { return &ref; };
};
// Some bonus things: we can implement try_lock
with std::try_to_lock
.
std::optional<MutexGuard> try_lock() {
if (std::unique_lock lock(mut, std::try_to_lock); lock.owns_lock()) {
return MutexGuard{val, std::move(lock)};
} else {
return std::nullopt;
}
}
// and also have a nice forwarding constructor for initialising val
.
template <typename... Args>
explicit Mutex(Args&&... args)
: val(std::forward<Args>(args)...), mut() {}
};
Pretty nifty.
The MutexGuard
was pretty basic when it came to the Mutex
,
but this pattern can also be used to encapsulate larger or complex locking patterns such as hand-over-hand locking
[lecture notes from csail.mit.edu].
In this case, a LinkedList
would return a GuardedIterator
,
hopefully written while fulfilling the C++ named requirements for iterators,
so algorithms may be used on the LinkedList
.
I think this would be a good teaching tool for students to learn RAII concepts.