Generic recursive mutex in standard 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 are recursive_mutex and recursive_timed_mutex starting with C ++ 11 I need shared_recursive_mutex . Am I missing something in the standard, or do I need to wait another three years for the standardized version?

If there is currently no such object, would there be a simple (first priority) and efficient (second priority) implementation of such a function using only standard C ++?

+8
c ++ multithreading mutex c ++ 11 c ++ 17
source share
4 answers

The recursive property of the mutex works with the term owner , which is not correct in the case of shared_mutex: several threads can have .lock_shared() called at the same time.

Assuming the owner as a thread that calls .lock() (not .lock_shared() !), An implementation of a recursive shared mutex can simply be obtained 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 .owner field must be declared atomic, because in the .lock() method it is checked without protection against simultaneous access.

If you want to call the .lock_shared() method .lock_shared() , you need to maintain an owner card , and access to this card must be protected using some additional mutex.

Allow a thread with active .lock() to call .lock_shared() to make the implementation more complicated.

Finally, permission to push a stream from .lock_shared() to .lock() is no-no , since this leads to a possible deadlock when two threads try to execute it came.


Again, the semantics of recursive generic mutexes will be very fragile, so it's best not to use it at all.

+3
source share

If you are on a Linux / POSIX platform, you're in luck because C ++ mutexes are modeled after POSIX. POSIX provides more features, including a recursive, generic process, and more. And porting POSIX primitives to C ++ classes is straightforward.

A good entry point to the POSIX thread documentation .

+2
source share

Here is a quick streamline around 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 approximately the same thing, and the guarded object is available only when the mutex is locked.

Stealing from @tsyvarev's answer (with some minor changes):

 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 remained as an exercise.

Both lock and unlock the joint lock of the mutex twice (this is safe, because the branches are actually about “this is the mutex control thread”, and the other thread cannot change this answer from “no” to “yes” or vice versa). You can do this with a single lock with ->* instead of -> , which will make it faster (due to some complexity in logic).


The above does not support having an exclusive lock, and then a general lock. It's complicated. It cannot maintain a common lock, and then updates to a unique lock, because in principle it is impossible to stop it from a deadlock when 2 threads try to do this.

This last issue may be the reason why recursive generic mutexes are a bad idea.

+2
source share

You can create a common recursive mutex using existing primitives. I do not recommend doing this.

It's not easy, and packaging an existing POSIX implementation (or whatever is native to your platform) is likely to be more efficient.

If you decide to write your own implementation, making it effective, it still depends on the specific details of the platform, so you either write an interface with a different implementation for each platform, or you choose the platform and can just as easily use your own (POSIX or other) objects.

Of course, I will not offer a recursive implementation of read / write locks, because this is an absolutely unreasonable amount of work to respond to a stack overflow.

+1
source share

All Articles