How can I implement a C ++ read-write lock using a single unlock method that can be called a reader or a writer?
I am working on a project that requires the use of specific OS abstractions and I need to implement read / write locking using their semaphore and mutex. I currently have a setup in the format:
class ReadWriteLock
{
public:
ReadWriteLock(uint32_t maxReaders);
~ReadWriteLock();
uint32_t GetMaxReaders() const;
eResult GetReadLock(int32_t timeout);
eResult GetWriteLock(int32_t timeout);
eResult Unlock();
private:
uint32_t m_MaxReaders;
Mutex* m_WriterMutex;
Semaphore* m_ReaderSemaphore;
};
In this implementation, I need to use this unlocking method to unlock the write and release all the reader semaphore slots, or just unbind the reader semaphore slot, however I am afraid as I cannot imagine an implementation that would work in all cases. How can I make this work in a given setup? I know this is possible as POSIX was able to implement a generic unlock method in its implementation, but I can't find any indication of how this was done, so appreciate any information people can share.
Please note that I cannot use C ++ 11 or other OS primitives.
source to share
Well, define two functions UnlockRead
and UnlockWrite
.
I believe you don't need both (Write / Read) access at the same time in the same place. Therefore, I suggest having two other classes for blocking access:
class ReadWriteAccess
{
public:
ReadWriteAccess(uint32_t maxReaders);
~ReadWriteAccess();
uint32_t GetMaxReaders() const;
uint32_t GetMaxReaders() const;
eResult GetReadLock(int32_t timeout);
eResult GetWriteLock(int32_t timeout);
eResult UnlockWrite();
eResult UnlockRead();
private:
uint32_t m_MaxReaders;
Mutex* m_WriterMutex;
Semaphore* m_ReaderSemaphore;
};
And have separate classes for blocking read and write and use RAII to be always safe:
class ReadLock
{
public:
ReadLock(ReadWriteAccess& access, int32_t timeout) : access(access)
{
result = access.GetReadLock(timeout);
}
eResult getResult() const { return result; }
~ReadLock()
{
if (result)
access.UnlockRead();
}
private:
ReadWriteAccess& access;
eResult result;
};
and use like this:
T someResource;
ReadWriteAccess someResourceGuard;
void someFunction()
{
ReadLock lock(someResourceGuard);
if (lock.getResult())
cout << someResource; // it is safe to read something from resource
}
Of course, a very similar implementation that you can easily write yourself for WriteLock
As the OP insisted on comments to have a "one" unlock - consider the downsides:
Let's assume that some stack of recent calls to Lock functions is implemented:
class ReadWriteLock
{
public:
ReadWriteLock(uint32_t maxReaders);
~ReadWriteLock();
uint32_t GetMaxReaders() const;
eResult GetReadLock(int32_t timeout)
{
eResult result = GetReadLockImpl(timestamp);
if (result)
lockStack.push(READ);
}
eResult GetWriteLock(int32_t timeout)
{
eResult result = GetWriteLockImpl(timestamp);
if (result)
lockStack.push(WRITE);
}
eResult Unlock()
{
LastLockMode lockMode = lockStack.top();
lockStack.pop();
if (lockMode == READ)
UnlockReadImpl();
else
UnlockWriteImpl();
}
private:
uint32_t m_MaxReaders;
Mutex* m_WriterMutex;
Semaphore* m_ReaderSemaphore;
enum Mode { READ, WRITE };
std::stack<Mode> lockStack;
};
But the above will only work in a single threaded application. And a single threaded application never needs any locks.
So - you must have a multithreaded stack - for example:
template <typename Value>
class MultiThreadStack
{
public:
void push(Value)
{
stackPerThread[getThreadId()].push(value);
}
Value top()
{
return stackPerThread[getThreadId()].top();
}
void pop()
{
stackPerThread[getThreadId()].pop();
}
private:
ThreadId getThreadId() { return /* your system way to get thread id*/; }
std::map<ThreadId, std::stack<Value>> stackPerThread;
};
So MultiThreadStack
don't use std :: stack in ReadWriteLock
.
But for the std::map
above std::map
need to ReadWriteLock
block access to it from multithreaded streams - so if you know all of your streams before starting to use this material (pre-registration) or you fall into the same problem that is described here . So my advice is - if you can - change your design.
source to share
When a lock is successfully acquired, the type is known: either you have many readers or one writer, you cannot have both readers and writers working with a reliably acquired lock.
Thus, it is sufficient to keep the current blocking mode on a successful call lock
and all subsequent calls unlock
(potentially a lot if read permission was granted, only one is valid if a write-and-write was requested) will be in this mode.
source to share