Working on a project that I currently have, I’ve found myself needing some mutual exclusion in dealing with some hardware devices. I could
have gone the singleton
route, where I only keep one device representation in memory at a time, but the internet and stack overflow have
both told me how I am oh so wrong for wanting that.
Thinking then about how I can support multiple devices in the future, while maintaining a single-device for now in a thread-safe manner, I’m forced
to use mutexes to protect critical sections of code. With this approach, the only global variable that I have is an std::mutex
which I use to
perform my locking.
So, to deal with that std::mutex
, I’ve come across something cool, std::lock_guard
. I wanted semantics that would roughly match the following
Java code.
try {
this.lock.lock();
doThing();
} finally {
this.lock.unlock();
}
This would cause doThing()
to only be executed by one calling thread at a time, with the rest being gated by this.lock.lock()
. finally
in Java
allows us to ensure that a block of code runs no matter what. In this case, we are releasing the lock.
std::lock_guard
uses a similar mechanism to ensure that locks are released, but it’s more like Golang’s defer
.
When the acquired lock goes out of scope, it is released! In ways, it may even be simpler than defer
.
So, with this new knowledge, I was able to write code like this:
bool Device::is_connected()
{
const std::lock_guard<std::mutex> lock( Device::m_device_mutex ); // m_device_mutex is a static variable in Device
return this->m_device != nullptr;
}
The lock is acquired by that first line, and it is released by going out of scope. This leads to very clean, concise code. I’ve in effect implemented a singleton too
by using the static Device::m_device_mutex
along with a std::shared_ptr
to my hardware.