A shared recursive mutex in standard C++ - c++

There is a shared_mutex class planned for C++17. And shared_timed_mutex already in C++14. (Who knows why they came in that order, but whatever.) Then there is a recursive_mutex and a recursive_timed_mutex since C++11. What I need is a shared_recursive_mutex. Did I miss something in the standard or do I have to wait another three years for a standardized version of that?
If there is currently no such facility, what would be a simple (first priority) and efficient (2nd priority) implementation of such a feature using standard C++ only?

Recursive property of the mutex operates with the term "owner", which in case of a shared_mutex is not well-defined: several threads may have .lock_shared() called at the same time.
Assuming "owner" to be a thread which calls .lock() (not .lock_shared()!), an implementation of the recursive shared mutex can be simply derived from shared_mutex:
class shared_recursive_mutex: public shared_mutex
{
public:
void lock(void) {
std::thread::id this_id = std::this_thread::get_id();
if(owner == this_id) {
// recursive locking
count++;
}
else {
// normal locking
shared_mutex::lock();
owner = this_id;
count = 1;
}
}
void unlock(void) {
if(count > 1) {
// recursive unlocking
count--;
}
else {
// normal unlocking
owner = std::thread::id();
count = 0;
shared_mutex::unlock();
}
}
private:
std::atomic<std::thread::id> owner;
int count;
};
The field .owner needs to be declared as atomic, because in the .lock() method this field is checked without a protection from the concurrent access.
If you want to recursively call .lock_shared() method, you need to maintain a map of owners, and accesses to that map should be protected with some additional mutex.
Allowing a thread with active .lock() to call .lock_shared() makes implementation to be more complex.
Finally, allowing a thread to advance locking from .lock_shared() to .lock() is no-no, as it leads to possible deadlock when two threads attempt to perform that advancing.
Again, semantic of recursive shared mutex would be very fragile, so it is better to not use it at all.

If you are on Linux / POSIX platform, you are in luck because C++ mutexes are modelled after POSIX ones. The POSIX ones provide more features, including being recursive, process shared and more. And wrapping POSIX primitives into C++ classes is straight-forward.
Good entry point into POSIX threads documentation.

Here is a quick thread-safety wrapper around a type T:
template<class T, class Lock>
struct lock_guarded {
Lock l;
T* t;
T* operator->()&&{ return t; }
template<class Arg>
auto operator[](Arg&&arg)&&
-> decltype(std::declval<T&>()[std::declval<Arg>()])
{
return (*t)[std::forward<Arg>(arg)];
}
T& operator*()&&{ return *t; }
};
constexpr struct emplace_t {} emplace {};
template<class T>
struct mutex_guarded {
lock_guarded<T, std::unique_lock<std::mutex>>
get_locked() {
return {{m},&t};
}
lock_guarded<T const, std::unique_lock<std::mutex>>
get_locked() const {
return {{m},&t};
}
lock_guarded<T, std::unique_lock<std::mutex>>
operator->() {
return get_locked();
}
lock_guarded<T const, std::unique_lock<std::mutex>>
operator->() const {
return get_locked();
}
template<class F>
std::result_of_t<F(T&)>
operator->*(F&& f) {
return std::forward<F>(f)(*get_locked());
}
template<class F>
std::result_of_t<F(T const&)>
operator->*(F&& f) const {
return std::forward<F>(f)(*get_locked());
}
template<class...Args>
mutex_guarded(emplace_t, Args&&...args):
t(std::forward<Args>(args)...)
{}
mutex_guarded(mutex_guarded&& o):
t( std::move(*o.get_locked()) )
{}
mutex_guarded(mutex_guarded const& o):
t( *o.get_locked() )
{}
mutex_guarded() = default;
~mutex_guarded() = default;
mutex_guarded& operator=(mutex_guarded&& o)
{
T tmp = std::move(o.get_locked());
*get_locked() = std::move(tmp);
return *this;
}
mutex_guarded& operator=(mutex_guarded const& o):
{
T tmp = o.get_locked();
*get_locked() = std::move(tmp);
return *this;
}
private:
std::mutex m;
T t;
};
You can use either:
mutex_guarded<std::vector<int>> guarded;
auto s0 = guarded->size();
auto s1 = guarded->*[](auto&&e){return e.size();};
both do roughly the same thing, and the object guarded is only accessed when the mutex is locked.
Stealing from #tsyvarev 's answer (with some minor changes) we get:
class shared_recursive_mutex
{
std::shared_mutex m
public:
void lock(void) {
std::thread::id this_id = std::this_thread::get_id();
if(owner == this_id) {
// recursive locking
++count;
} else {
// normal locking
m.lock();
owner = this_id;
count = 1;
}
}
void unlock(void) {
if(count > 1) {
// recursive unlocking
count--;
} else {
// normal unlocking
owner = std::thread::id();
count = 0;
m.unlock();
}
}
void lock_shared() {
std::thread::id this_id = std::this_thread::get_id();
if (shared_counts->count(this_id)) {
++(shared_count.get_locked()[this_id]);
} else {
m.lock_shared();
shared_count.get_locked()[this_id] = 1;
}
}
void unlock_shared() {
std::thread::id this_id = std::this_thread::get_id();
auto it = shared_count->find(this_id);
if (it->second > 1) {
--(it->second);
} else {
shared_count->erase(it);
m.unlock_shared();
}
}
private:
std::atomic<std::thread::id> owner;
std::atomic<std::size_t> count;
mutex_guarded<std::map<std::thread::id, std::size_t>> shared_counts;
};
try_lock and try_lock_shared left as an exercise.
Both lock and unlock shared lock the mutex twice (this is safe, as the branches are really about "is this thread in control of the mutex", and another thread cannot change that answer from "no" to "yes" or vice versa). You could do it with one lock with ->* instead of ->, which would make it faster (at the cost of some complexity in the logic).
The above does not support having an exclusive lock, then a shared lock. That is tricky. It cannot support having a shared lock, then upgrading to an unique lock, because that is basically impossible to stop it from deadlocking when 2 threads try that.
That last issue may be why recursive shared mutexes are a bad idea.

It is possible to construct a shared recursive mutex using existing primitives. I don't recommend doing it, though.
It isn't simple, and wrapping the existing POSIX implementation (or whatever is native to your platform) is very likely to be more efficient.
If you do decide to write your own implementation, making it efficient still depends on platform-specific details, so you're either writing an interface with a different implementation for each platform, or you're selecting a platform and could just as easily use the native (POSIX or whatever) facilities instead.
I'm certainly not going to provide a sample recursive read/write lock implementation, because it's a wholly unreasonable amount of work for an Stack Overflow answer.

Sharing my implementation, no promises
recursive_shared_mutex.h
#ifndef _RECURSIVE_SHARED_MUTEX_H
#define _RECURSIVE_SHARED_MUTEX_H
#include <thread>
#include <mutex>
#include <map>
struct recursive_shared_mutex
{
public:
recursive_shared_mutex() :
m_mtx{}, m_exclusive_thread_id{}, m_exclusive_count{ 0 }, m_shared_locks{}
{}
void lock();
bool try_lock();
void unlock();
void lock_shared();
bool try_lock_shared();
void unlock_shared();
recursive_shared_mutex(const recursive_shared_mutex&) = delete;
recursive_shared_mutex& operator=(const recursive_shared_mutex&) = delete;
private:
inline bool is_exclusive_locked()
{
return m_exclusive_count > 0;
}
inline bool is_shared_locked()
{
return m_shared_locks.size() > 0;
}
inline bool can_exclusively_lock()
{
return can_start_exclusive_lock() || can_increment_exclusive_lock();
}
inline bool can_start_exclusive_lock()
{
return !is_exclusive_locked() && (!is_shared_locked() || is_shared_locked_only_on_this_thread());
}
inline bool can_increment_exclusive_lock()
{
return is_exclusive_locked_on_this_thread();
}
inline bool can_lock_shared()
{
return !is_exclusive_locked() || is_exclusive_locked_on_this_thread();
}
inline bool is_shared_locked_only_on_this_thread()
{
return is_shared_locked_only_on_thread(std::this_thread::get_id());
}
inline bool is_shared_locked_only_on_thread(std::thread::id id)
{
return m_shared_locks.size() == 1 && m_shared_locks.find(id) != m_shared_locks.end();
}
inline bool is_exclusive_locked_on_this_thread()
{
return is_exclusive_locked_on_thread(std::this_thread::get_id());
}
inline bool is_exclusive_locked_on_thread(std::thread::id id)
{
return m_exclusive_count > 0 && m_exclusive_thread_id == id;
}
inline void start_exclusive_lock()
{
m_exclusive_thread_id = std::this_thread::get_id();
m_exclusive_count++;
}
inline void increment_exclusive_lock()
{
m_exclusive_count++;
}
inline void decrement_exclusive_lock()
{
if (m_exclusive_count == 0)
{
throw std::logic_error("Not exclusively locked, cannot exclusively unlock");
}
if (m_exclusive_thread_id == std::this_thread::get_id())
{
m_exclusive_count--;
}
else
{
throw std::logic_error("Calling exclusively unlock from the wrong thread");
}
}
inline void increment_shared_lock()
{
increment_shared_lock(std::this_thread::get_id());
}
inline void increment_shared_lock(std::thread::id id)
{
if (m_shared_locks.find(id) == m_shared_locks.end())
{
m_shared_locks[id] = 1;
}
else
{
m_shared_locks[id] += 1;
}
}
inline void decrement_shared_lock()
{
decrement_shared_lock(std::this_thread::get_id());
}
inline void decrement_shared_lock(std::thread::id id)
{
if (m_shared_locks.size() == 0)
{
throw std::logic_error("Not shared locked, cannot shared unlock");
}
if (m_shared_locks.find(id) == m_shared_locks.end())
{
throw std::logic_error("Calling shared unlock from the wrong thread");
}
else
{
if (m_shared_locks[id] == 1)
{
m_shared_locks.erase(id);
}
else
{
m_shared_locks[id] -= 1;
}
}
}
std::mutex m_mtx;
std::thread::id m_exclusive_thread_id;
size_t m_exclusive_count;
std::map<std::thread::id, size_t> m_shared_locks;
std::condition_variable m_cond_var;
};
#endif
recursive_shared_mutex.cpp
#include "recursive_shared_mutex.h"
#include <condition_variable>
void recursive_shared_mutex::lock()
{
std::unique_lock sync_lock(m_mtx);
m_cond_var.wait(sync_lock, [this] { return can_exclusively_lock(); });
if (is_exclusive_locked_on_this_thread())
{
increment_exclusive_lock();
}
else
{
start_exclusive_lock();
}
}
bool recursive_shared_mutex::try_lock()
{
std::unique_lock sync_lock(m_mtx);
if (can_increment_exclusive_lock())
{
increment_exclusive_lock();
return true;
}
if (can_start_exclusive_lock())
{
start_exclusive_lock();
return true;
}
return false;
}
void recursive_shared_mutex::unlock()
{
{
std::unique_lock sync_lock(m_mtx);
decrement_exclusive_lock();
}
m_cond_var.notify_all();
}
void recursive_shared_mutex::lock_shared()
{
std::unique_lock sync_lock(m_mtx);
m_cond_var.wait(sync_lock, [this] { return can_lock_shared(); });
increment_shared_lock();
}
bool recursive_shared_mutex::try_lock_shared()
{
std::unique_lock sync_lock(m_mtx);
if (can_lock_shared())
{
increment_shared_lock();
return true;
}
return false;
}
void recursive_shared_mutex::unlock_shared()
{
{
std::unique_lock sync_lock(m_mtx);
decrement_shared_lock();
}
m_cond_var.notify_all();
}
If a thread owns a shared lock, it may also obtain an exclusive lock without giving up it's shared lock. (This of course requires no other thread currently has a shared or exclusive lock)
Vice-versa, a thread which owns an exclusive lock may obtain a shared lock.
Interestingly, these properties also allow locks to be upgradable/downgradable.
Temporarily upgrading lock:
recusrive_shared_mutex mtx;
foo bar;
mtx.lock_shared();
if (bar.read() == x)
{
mtx.lock();
bar.write(y);
mtx.unlock();
}
mtx.unlock_shared();
Downgrading from an exclusive lock to a shared lock
recusrive_shared_mutex mtx;
foo bar;
mtx.lock();
bar.write(x);
mtx.lock_shared();
mtx.unlock();
while (bar.read() != y)
{
// Something
}
mtx.unlock_shared();

I searched for a C++ read-write-lock and came across this related question. We did need exactly such a shared_recursive_mutex to control access to our "database" class from multiple threads. So, for completeness: If you are looking for another implementation example (like I was), you may want to consider this link too: shared_recursive_mutex implementation using C++17 (on github).
Features
C++17
Single Header
Dependency-free
It has a disadvantage though: static thread_local members specialized for a PhantomType class via template. So, you can't really use this shared_recursive_mutex in multiple separate instances of the same (PhantomType) class. Try it, if that is no restriction for you.

The following implementation supports first having a unique_lock and then acquiring an additional shared_lock in the same thread:
#include <shared_mutex>
#include <thread>
class recursive_shared_mutex: public std::shared_mutex {
public:
void lock() {
if (owner_ != std::this_thread::get_id()) {
std::shared_mutex::lock();
owner_ = std::this_thread::get_id();
}
++count_;
}
void unlock() {
--count_;
if (count_ == 0) {
owner_ = std::thread::id();
std::shared_mutex::unlock();
}
}
void lock_shared() {
if (owner_ != std::this_thread::get_id()) {
std::shared_mutex::lock_shared();
}
}
void unlock_shared() {
if (owner_ != std::this_thread::get_id()) {
std::shared_mutex::unlock_shared();
}
}
private:
std::atomic<std::thread::id> owner_;
std::atomic_uint32_t count_ = 0;
};

Related

A deadlock when use boost shared_mutex

I have an very delicate deadlock bug when I use the shared_ptr with custom Deleter and boost::share_mutex:
First there is an WorkManager class to manage Work object:
class Work;
class WorkManager {
public:
static WorkManager& instance() {
static WorkManager instance;
return instance;
}
std::shared_ptr<Work> get(int id);
std::shared_ptr<Work> create(int id);
void update(unsigned int diff);
private:
void _remove(int id, Work* t);
WorkManager();
mutable boost::shared_mutex _mutex;
std::map<int, std::weak_ptr<Work>> _works;
};
void WorkManager::_remove(int id, Work* t)
{
BOOST_ASSERT(t);
std::lock_guard<boost::shared_mutex> lock(_mutex);
_works.erase(id);
delete t;
}
std::shared_ptr<Work> WorkManager::get(int id) {
boost::shared_lock<boost::shared_mutex> lock(_mutex);
auto iter = _works.find(id);
if(iter == _works.end())
return nullptr;
return iter->second.lock();
}
std::shared_ptr<Work> WorkManager::create(int id) {
Work* t = new Work(id);
std::shared_ptr<Work> res;
res.reset(t, std::bind(&WorkManager::_remove, this, t->id(), std::placeholders::_1));
std::lock_guard<boost::shared_mutex> lock(_mutex);
_works[res->id()] = res;
return res;
}
void WorkManager::update(unsigned int diff) {
boost::shared_lock<boost::shared_mutex> lock(_mutex);
for(auto& t : _works)
{
if(std::shared_ptr<Work> work = t.second.lock())
work->update(diff);
}
}
WorkManager::update() is triggered by a timer, called at a fixed interval like 100ms. Because WorkManager doesn't hold shared_ptr<Work> but only weak_ptr<Work>, after work->update(diff); returns the work may be destroyed because there may be no shared_ptr<Work> holding this particular work any more. Then the WorkManager::_remove() will be called via the custom deleter for the shared_ptr<Work>. Notice the update thread already hold a read lock, in _remove(), it needs to acquire a write lock. However it must release the read lock before it can acquire this write lock. So this thread is dead.
This problem means that I cannot write any Manager's function do things like:
boost::shared_lock<boost::shared_mutex> lock(_mutex);
for(auto& t : _works)
{
if(std::shared_ptr<Work> w = t.second.lock())
w->do_something();
}
How to solve this? I figure out a solution is:
std::vector<std::shared_ptr<Work>> tmp;
{
boost::shared_lock<boost::shared_mutex> lock(_mutex);
for(auto& t : _works)
{
if(std::shared_ptr<Work> w = t.second.lock())
{
w->do_something();
tmp.push_back(w); //prevent w destroy before the read lock unlock
}
}
}
However this is error-prone, if someone forget this trick.

Is there something wrong with this rwLock implementation?

My program is deadlocking, and I have no idea why, given that it won't do it when I run it in a debugger, so my first suspect is my rwLock, I wrote my own version because I only wanted to use standard libraries--I don't think a rwLock is included until C++17--and this isn't the sort of thing I normally do.
class RwLock
{
std::mutex mutex;
std::unique_lock<std::mutex> unique_lock;
std::condition_variable condition;
int reading_threads;
bool writing_threads;
public:
RwLock();
~RwLock();
void read_lock();
void read_unlock();
void write_lock();
void write_unlock();
};
RwLock::RwLock() :
mutex(),
unique_lock(mutex, std::defer_lock),
condition(),
reading_threads(0),
writing_threads(false)
{
}
RwLock::~RwLock()
{
//TODO: find something smarter to do here.
write_lock();
}
void RwLock::read_lock()
{
unique_lock.lock();
while(writing_threads)
{
condition.wait(unique_lock);
}
++reading_threads;
unique_lock.unlock();
}
void RwLock::read_unlock()
{
unique_lock.lock();
if(--reading_threads == 0)
{
condition.notify_all();
}
unique_lock.unlock();
}
void RwLock::write_lock()
{
unique_lock.lock();
while(writing_threads)
{
condition.wait(unique_lock);
}
writing_threads = 1;
while(reading_threads)
{
condition.notify_all();
}
unique_lock.unlock();
}
void RwLock::write_unlock()
{
unique_lock.lock();
writing_threads = 0;
condition.notify_all();
unique_lock.unlock();
}
std::shared_timed_mutex exists prior to C++17: in C++14.
Use it instead, it will have fewer bugs and be faster almost certainly.
C++17 introduces shared_mutex which can be even faster. But I strongly doubt your ability to implement a faster shared rwlock than shared_timed_mutex using C++ standard primitives.
Looks good except for two issues in this code:
void RwLock::write_lock()
{
unique_lock.lock();
while(writing_threads)
{
condition.wait(unique_lock);
}
writing_threads = 1;
while(reading_threads)
{
condition.notify_all();
}
unique_lock.unlock();
}
First, you increment writing_threads too late. A reader could sneak in. It's possible that you don't mind or even want this, but typically this is undesired.
Second, your notify in the last while loop should be a wait. Putting it together, we get:
void RwLock::write_lock()
{
unique_lock.lock();
++writing_threads;
while((writing_threads > 1) || (reading_threads > 0))
{
condition.wait(unique_lock);
}
unique_lock.unlock();
}
void RwLock::write_unlock()
{
unique_lock.lock();
--writing_threads; // note change here
condition.notify_all();
unique_lock.unlock();
}
This is actually a bit simpler, which is nice.

Mutex/Lock with scope/codeblock

I remember seeing it in some conference, but can't find any information on this.
I want something like:
lock(_somelock)
{
if (_someBool)
return;
DoStuff();
} // Implicit unlock
Instead of:
lock(_somelock);
if (_someBool)
{
unlock(_somelock);
return;
}
DoStuff();
unlock(_somelock);
As you can see the code gets very bloated with multiple early returns.
Obviously one could make another function to handle locking/unlocking, but it's a lot nicer no?
Possible with C++11 standard library?
Yes, you can use a std::lock_guard to wrap a mutex.
{
std::lock_guard<std::mutex> lock(your_mutex);
if (_someBool)
return;
DoStuff();
}
The standard idiom is to use a guard object whose lifetime encompasses the locked state of the mutex:
std::mutex m;
int shared_data;
// somewhere else
void foo()
{
int x = compute_something();
{
std::lock_guard<std::mutex> guard(m);
shared_data += x;
}
some_extra_work();
}
You can simply create an autolock of your own.
Class AutoLock
{
pthread_mutex_t *mpLockObj;
AutoLock(pthread_mutex_t& mpLockObj)
{
mpLockObj = &mpLockObj;
pthread_mutex_lock(mpLockObj);
}
~AutoLock()
{
pthread_mutex_unlock(mpLockObj);
}
};
use like:
#define LOCK(obj) AutoLock LocObj(obj);
int main()
{
pthread_mutex_t lock;
LOCK(lock);
return 0;
}

Threads in C++11

I'm updating my C++ skills to C++11. I'm up to threads, always a problem area. Consider this testing code:
// threaded.h
class MyThreadedClass
{
public:
MyThreadClass();
bool StartThread();
bool IsThreadDone();
inline void WorkThread();
private:
std::thread* workThread;
atomic<bool> threadDone;
}
// threaded.cpp
MyThreadedClass::MyThreadedClass() {
workThread = nullptr;
threadDone.store(true);
}
bool MyThreadedClass::StartThread() {
if (!threadDone.load()) { return false; }
threadDone.store(false);
workThread = new std::thread(&MyThreadedClass:WorkThread, this);
workThread->detach();
return true;
}
bool MyThreadedClass:IsThreadDone() {
return threadDone.load();
}
inline void MyThreadedClass::WorkThread() {
while (some_condition) { /*Do work*/ }
threadDone.store(true);
}
// main.cpp
int main() {
MyThreadedClass testInstance;
testInstance.StartThread();
for (int x = 0; x < 10; x++) {
do {
// This is sometimes true:
if (testInstance.StartThread()) { return 1;}
} while (!testInstance.IsThreadDone())
}
return 0;
}
I wanted to look at a worst case scenario for this type of code therefore I'm pounding it continually in main while waiting for thread to terminate. Sometimes the failure condition in main is triggered. As with many threading problems it's not consistent and therefore not easy to debug.
The threadDone variable is used because I access a file in my actual code and don't want multiple threads accessing the same file.
Insight into what I'm missing or ways to redesign this with C++11 idioms welcome.
With std::mutex instead of std::atomic, the implementation is quite simple.
class MyThreadedClass
{
std::mutex _mutex;
std::unique_ptr<std::thread> _thread;
bool _done{false};
public:
MyThreadClass() = default;
bool StartThread()
{
std::lock_guard<std::mutex> lock(_mutex);
if (_thread || _done) return false;
_thread = std::make_unique<std::thread>(&MyThreadedClass, this);
return true;
}
bool IsThreadDone()
{
std::lock_guard<std::mutex> lock(_mutex);
return _done;
}
void WorkThread()
{
// do some work
std::lock_guard<std::mutex> lock(_mutex);
_done = true;
}
}
The best reference / learning book I know of for C++11 concurrency is here: C++ Concurrency in Action: Practical Multithreading by Anthony Williams.
You have a race condition (though not a "data race") in your main() function, which I've transcribed, inlined, and simplified below:
void MyThreadedClass::WorkThread() {
threadDone.store(true);
}
int main() {
MyThreadedClass testInstance;
testInstance.threadDone.store(false);
new std::thread(&MyThreadedClass::WorkThread, &testInstance)->detach();
// This sometimes fails:
assert(!testInstance.threadDone.load());
return 0;
}
That assertion will fail if WorkThread happens to run to completion before the main thread is scheduled again. Since the standard doesn't constrain the scheduling, you'd need to write some code to block if you need the worker thread to wait.

In C++, is there a way to lock the object itself?

I have a stl map which I would like to be synchronized across several threads. Currently I have...
Function A (Modifies map)
void Modify(std::string value)
{
pthread_mutex_lock(&map_mutex);
my_map[value] = value;
pthread_mutex_unlock(&map_mutex);
}
Function B (Reads map)
std::string Read(std::string key)
{
std::string value;
pthread_mutex_lock(&map_mutex);
std::map<std::string, std::string>::iterator it = my_map.find(key);
pthread_mutex_unlock(&map_mutex);
if(it != my_map.end())
{
return it->second;
}
else
{
return "DNE";
}
}
This is synchronized across all threads, due to the mutex. However, I have to lock the mutex in Function B even though it is not modifying the map at all. Is there a way to lock the my_map object itself in function A, and not lock it in function B while keeping thread synchronization. This way, all instances/calls of Function B continue to run freely, so long as Function A is not being run?
Thanks
You don't just want to lock the container, you also want to lock accesses into the container i.e. any iterators or pointers into it. You need to move those accesses into the locked region of the code.
std::string Read(std::string key)
{
std::string value = "DNE";
pthread_mutex_lock(&map_mutex);
std::map<std::string, std::string>::iterator it = my_map.find(key);
if(it != my_map.end())
{
value = it->second;
}
pthread_mutex_unlock(&map_mutex);
return value;
}
There's really no practical way to do this from inside the object itself.
Warning: I have not compiled or tested any of this, but I've done similar things in the past.
Step one would be to control the mutex with class, as such:
class Lock {
public:
Lock(Mutex& mutex) {
pthread_mutex_lock(mutex);
}
~Lock(Mutex& mutex) {
pthread_mutex_unlock(mutex);
}
};
This saves you from all sorts of issues, for instance, if your map throws an exception.
Then your modify becomes:
void Modify(std::string value)
{
Lock(map_mutex);
my_map[value] = value;
}
Create a reference counted lock class:
class RefCntLock {
private:
static int count;
static Lock* lock;
public:
RefCountLock(Mutex& mutex) {
// probably want to check that the mutex matches prior instances.
if( !lock ) {
lock = new Lock(mutex);
count++;
}
}
~RefCountLock() {
--count;
if( count == 0 ) {
delete lock;
lock = NULL;
}
}
};
(Note: it'd be easy to generalize this to deal with multiple mutexes.)
In your read, use the RefCntLock class:
std::string Read(std::string key)
{
{
RefCntLock(&map_mutex);
std::map<std::string, std::string>::iterator it = my_map.find(key);
}
if(it != my_map.end())
{
return it->second;
}
else
{
return "DNE";
}
}
This means that each write gets a lock but all reads share a lock.
In the upcoming C++17 standard, you can use std::shared_mutex and std::shared_lock to allow multiple readers exclusive read access, and std::unique_lock to implement exclusive write access.
std::shared_mutex map_lock;
void Modify(std::string value)
{
auto write_lock = std::unique_lock(map_lock);
my_map[value] = value;
}
std::string Read(std::string key)
{
auto read_lock = std::shared_lock(map_lock);
auto it = my_map.find(key);
return (it != my_map.end()) ? it->second : "DNE";
}