I created the following class which provides an acquire_lock()
and release_lock()
function
class LockableObject {
public:
void acquire_lock() {
std::unique_lock<std::mutex> local_lock(m_mutex);
m_lock = std::move(local_lock);
}
void release_lock() {
m_lock.unlock();
}
private:
std::mutex m_mutex;
std::unique_lock<std::mutex> m_lock;
};
This class provides an acquire_lock
and release_lock
function. I have multiple threads accessing the same object and calling the acquire_lock
before performing any operations and then calling release_lock
once done as below.
void ThreadFunc(int ID, LockableObject* lckbleObj)
{
for (int i = 0; i < 1000; i++)
{
lckbleObj->acquire_lock();
std::cout << "Thread with ID = " << ID << "doing work" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
lckbleObj->release_lock();
}
}
void main()
{
const int numThreads = 10;
std::thread workerThreads[numThreads];
LockableObject *testObject = new LockableObject();
for (int i = 0; i < numThreads; i++)
{
workerThreads[i] = std::thread(ThreadFunc, i, testObject);
}
for (int i = 0; i < numThreads; i++)
{
workerThreads[i].join();
}
}
In the acquire_lock
function, I first try to lock the underlying mutex (m_mutex
) with a local stack std::unique_lock
object by passing it ( m_mutex
) in the constructor. I assume once the constructor for std::unique_lock
returns it has locked the mutex, I then move the unique_lock
on the stack to the member variable m_lock
.
This program is flawed in some basic way and during the call to release_lock
will result in the "unlock of unowned mutex"
, I seem to be missing something basic about std::unique_lock
and am looking for someone to correct my understanding.
See my comment about the lack of std::defer_lock
in the constructor. But you also have a race condition in your code.
The acquire_lock
function modifies the m_lock
under protection of the m_mutex
mutex. Thus, to ensure thread safety, no other thread can modify m_lock
except while holding m_mutex
.
But the release_lock
function modifies m_lock
while it is releasing that mutex. Thus, you do not have proper synchronization on m_lock
.
This is somewhat subtle to understand. This is the problem code:
m_lock.unlock();
Note that when this function is entered, m_mutex
is locked but during its execution, it both modifies m_lock
and releases m_mutex
in no particular guaranteed order. But m_mutex
protects m_lock
. So this is a race condition and not permitted.
It can be fixed as follows:
void release_lock() {
std::unique_lock<std::mutex> local_lock = std::move(m_lock);
local_lock.unlock();
}
Now, this first line of code modifies m_lock
but runs entirely with m_mutex
held. This avoids the race condition.
The unlock
can be removed if desired. The destructor of local_lock
will do it.
By the way, I would suggest changing the API. Instead of offering lock and unlock calls, instead have a way to create an object that owns a lock on this object. You can even use std::unique_lock<LockableObject>
if you want. Why create a new API that's worse than the one offered by the standard?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With