shared_mutex
C++ 17で計画されているクラス。そして shared_timed_mutex
すでにC++ 14にあります。 (なぜ彼らがその順序で来たのか誰が知っていますが、何でも。)そして recursive_mutex
および recursive_timed_mutex
C++ 11以降。必要なのはshared_recursive_mutex
。標準の何かを見逃しましたか、それとも標準化されたバージョンをさらに3年待つ必要がありますか?
現在そのような機能がない場合、標準のC++のみを使用したそのような機能の単純な(最優先)および効率的な(2番目の優先)実装は何でしょうか?
ミューテックスのRecursiveプロパティは、用語ownerで動作します。これは、shared_mutexは明確に定義されていません。複数のスレッドで同時に.lock_shared()
が呼び出される場合があります。
ownerを.lock()
(.lock_shared()
!ではない)を呼び出すスレッドとすると、再帰的共有ミューテックスの実装は_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;
};
_
フィールド_.owner
_はアトミックとして宣言する必要があります。これは、.lock()
メソッドでは、同時アクセスから保護せずにチェックされるためです。
.lock_shared()
メソッドを再帰的に呼び出したい場合は、所有者のマップを維持する必要があり、そのマップへのアクセスは追加のミューテックスで保護する必要があります。
アクティブな.lock()
を持つスレッドが.lock_shared()
を呼び出すことを許可すると、実装がより複雑になります。
最後に、スレッドにadvance.lock_shared()
から.lock()
へのロックを許可することはno-noです。これは、2つの場合にデッドロックが発生する可能性があるためです。スレッドはその前進を実行しようとします。
繰り返しますが、recursivesharedミューテックスのセマンティクスは非常に壊れやすいため、まったく使用しない方が良いです。
Linux/POSIXプラットフォームを使用している場合は、C++ミューテックスがPOSIXミューテックスをモデルにしているため、幸運です。 POSIXのものは、再帰的であること、プロセス共有など、より多くの機能を提供します。また、POSIXプリミティブをC++クラスにラップするのは簡単です。
タイプ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;
};
次のいずれかを使用できます。
mutex_guarded<std::vector<int>> guarded;
auto s0 = guarded->size();
auto s1 = guarded->*[](auto&&e){return e.size();};
どちらもほぼ同じことを行い、保護されたオブジェクトは、ミューテックスがロックされている場合にのみアクセスされます。
@tsyvarevの回答から(いくつかの小さな変更を加えて)盗むと、次のようになります。
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
とtry_lock_shared
は演習として残しました。
ロックとロック解除の両方がミューテックスを2回ロックします(ブランチは実際には「このスレッドがミューテックスを制御している」ことであり、別のスレッドはその答えを「いいえ」から「はい」に、またはその逆に変更できないため、これは安全です) 。 ->*
の代わりに->
を使用して1つのロックでそれを行うことができます。これにより、高速になります(ロジックが多少複雑になります)。
上記は、排他ロック、次に共有ロックの使用をサポートしていません。それはトリッキーです。共有ロックを使用してから一意のロックにアップグレードすることはサポートできません。これは、2つのスレッドがそれを試みたときにデッドロックを防ぐことは基本的に不可能だからです。
その最後の問題は、再帰的な共有ミューテックスが悪い考えである理由かもしれません。
is既存のプリミティブを使用して共有再帰ミューテックスを構築することは可能です。ただし、これを行うことはお勧めしません。
これは単純なことではなく、既存のPOSIX実装(またはプラットフォームにネイティブなもの)をラップする方が効率的である可能性が非常に高くなります。
do独自の実装を作成することにした場合でも、効率を上げるにはプラットフォーム固有の詳細に依存するため、プラットフォームごとに異なる実装のインターフェイスを作成するか、プラットフォームであり、代わりにネイティブ(POSIXなど)機能を簡単に使用できます。
スタックオーバーフローの回答にはまったく不合理な量の作業であるため、サンプルの再帰的な読み取り/書き込みロックの実装を提供するつもりはありません。
私の実装を共有する、約束はありません
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();
}
スレッドが共有ロックを所有している場合、そのスレッドは共有ロックを放棄せずに排他ロックを取得することもできます。 (もちろん、これは他のスレッドが現在共有または排他的ロックを持っている必要はありません)
逆に、排他ロックを所有するスレッドは共有ロックを取得する場合があります。
興味深いことに、これらのプロパティを使用すると、ロックをアップグレード/ダウングレードすることもできます。
ロックを一時的にアップグレードする:
recusrive_shared_mutex mtx;
foo bar;
mtx.lock_shared();
if (bar.read() == x)
{
mtx.lock();
bar.write(y);
mtx.unlock();
}
mtx.unlock_shared();
排他ロックから共有ロックへのダウングレード
recusrive_shared_mutex mtx;
foo bar;
mtx.lock();
bar.write(x);
mtx.lock_shared();
mtx.unlock();
while (bar.read() != y)
{
// Something
}
mtx.unlock_shared();