設計上、std::mutex
移動もコピーもできません。これはA
、ミューテックスを保持するクラスがデフォルトの移動コンストラクターを受け取らないことを意味します。
このタイプをA
スレッドセーフな方法で移動可能にするにはどうすればよいですか?
設計上、std::mutex
移動もコピーもできません。これはA
、ミューテックスを保持するクラスがデフォルトの移動コンストラクターを受け取らないことを意味します。
このタイプをA
スレッドセーフな方法で移動可能にするにはどうすればよいですか?
std::lock_guard
はメソッドスコープのみです。
回答:
少しのコードから始めましょう:
class A
{
using MutexType = std::mutex;
using ReadLock = std::unique_lock<MutexType>;
using WriteLock = std::unique_lock<MutexType>;
mutable MutexType mut_;
std::string field1_;
std::string field2_;
public:
...
C ++ 11では実際には利用されないが、C ++ 14でははるかに便利になる、かなり示唆に富む型エイリアスをそこに配置しました。しばらくお待ちください。そこに着きます。
あなたの質問は要約すると:
このクラスのムーブコンストラクターとムーブ代入演算子を作成するにはどうすればよいですか?
まず、moveコンストラクターから始めます。
コンストラクターの移動
メンバーmutex
が作成されていることに注意してくださいmutable
。厳密に言えば、これはムーブメンバーには必要ありませんが、コピーメンバーも必要だと思います。そうでない場合は、ミューテックスを作成する必要はありません。mutable
。
を構築するときA
、をロックする必要はありませんthis->mut_
。ただしmut_
、構築元のオブジェクトのをロックする必要があります(移動またはコピー)。これは次のように行うことができます。
A(A&& a)
{
WriteLock rhs_lk(a.mut_);
field1_ = std::move(a.field1_);
field2_ = std::move(a.field2_);
}
this
最初にのメンバーをデフォルトで作成し、a.mut_
ロックされた後でのみ値を割り当てる必要があることに注意してください。
ムーブ代入
他のスレッドが代入式のlhsまたはrhsのいずれかにアクセスしているかどうかがわからないため、ムーブ代入演算子はかなり複雑です。そして、一般的に、次のシナリオから保護する必要があります。
// Thread 1
x = std::move(y);
// Thread 2
y = std::move(x);
上記のシナリオを正しく保護するムーブ代入演算子は次のとおりです。
A& operator=(A&& a)
{
if (this != &a)
{
WriteLock lhs_lk(mut_, std::defer_lock);
WriteLock rhs_lk(a.mut_, std::defer_lock);
std::lock(lhs_lk, rhs_lk);
field1_ = std::move(a.field1_);
field2_ = std::move(a.field2_);
}
return *this;
}
std::lock(m1, m2)
2つのミューテックスを次々にロックするのではなく、を使用してロックする必要があることに注意してください。それらを次々にロックすると、上記のように2つのスレッドが2つのオブジェクトを逆の順序で割り当てると、デッドロックが発生する可能性があります。ポイントstd::lock
は、そのデッドロックを回避することです。
コピーコンストラクタ
コピーメンバーについては質問しませんでしたが、今すぐ話し合うこともできます(そうでない場合は、誰かが必要になります)。
A(const A& a)
{
ReadLock rhs_lk(a.mut_);
field1_ = a.field1_;
field2_ = a.field2_;
}
コピーコンストラクターは、ReadLock
エイリアスが代わりに使用されることを除いて、移動コンストラクターによく似ています。WriteLock
ます。現在、これらは両方ともエイリアスstd::unique_lock<std::mutex>
であるため、実際には何の違いもありません。
しかし、C ++ 14では、次のように言うオプションがあります。
using MutexType = std::shared_timed_mutex;
using ReadLock = std::shared_lock<MutexType>;
using WriteLock = std::unique_lock<MutexType>;
これは最適化かもしれませんが、確実ではありません。あなたはそれがそうであるかどうかを決定するために測定する必要があります。ただし、この変更により、複数のスレッドの同じrhsからコンストラクトを同時にコピーできます。C ++ 11ソリューションでは、rhsが変更されていない場合でも、このようなスレッドをシーケンシャルにする必要があります。
割り当てのコピー
完全を期すために、ここにコピー代入演算子があります。これは、他のすべてについて読んだ後、かなり自明であるはずです。
A& operator=(const A& a)
{
if (this != &a)
{
WriteLock lhs_lk(mut_, std::defer_lock);
ReadLock rhs_lk(a.mut_, std::defer_lock);
std::lock(lhs_lk, rhs_lk);
field1_ = a.field1_;
field2_ = a.field2_;
}
return *this;
}
や。。など。
A
複数のスレッドが一度にそれらを呼び出すことができると予想される場合は、の状態にアクセスする他のメンバーまたは空き関数も保護する必要があります。たとえば、ここにありますswap
:
friend void swap(A& x, A& y)
{
if (&x != &y)
{
WriteLock lhs_lk(x.mut_, std::defer_lock);
WriteLock rhs_lk(y.mut_, std::defer_lock);
std::lock(lhs_lk, rhs_lk);
using std::swap;
swap(x.field1_, y.field1_);
swap(x.field2_, y.field2_);
}
}
std::swap
ジョブの実行に依存している場合、ロックは間違った粒度で行われ、std::swap
内部で実行される3つの移動の間でロックとロック解除が行われることに注意してください。
実際、考えるswap
ことで、「スレッドセーフ」に提供する必要のあるAPIについての洞察を得ることができA
ます。これは、「ロックの粒度」の問題のため、一般に「非スレッドセーフ」APIとは異なります。
また、「セルフスワップ」から保護する必要があることにも注意してください。「セルフスワップ」は何もしないでください。セルフチェックがないと、同じミューテックスを再帰的にロックします。これは、std::recursive_mutex
forを使用してセルフチェックなしで解決することもできMutexType
ます。
更新
以下のコメントでは、Yakkは、コピーおよび移動コンストラクターでデフォルトでコンストラクターを作成する必要があることにかなり不満を持っています(そして彼にはポイントがあります)。この問題について十分に強く感じて、メモリを費やしても構わないと思っている場合は、次のように回避できます。
必要なロックタイプをデータメンバーとして追加します。これらのメンバーは、保護されているデータの前に来る必要があります。
mutable MutexType mut_;
ReadLock read_lock_;
WriteLock write_lock_;
// ... other data members ...
そして、コンストラクター(コピーコンストラクターなど)で次のようにします。
A(const A& a)
: read_lock_(a.mut_)
, field1_(a.field1_)
, field2_(a.field2_)
{
read_lock_.unlock();
}
おっと、私がこの更新を完了する前に、Yakkは彼のコメントを消去しました。しかし、彼はこの問題を推し進め、この答えに解決策をもたらしたことは称賛に値します。
アップデート2
そして、dypはこの良い提案を思いつきました:
A(const A& a)
: A(a, ReadLock(a.mut_))
{}
private:
A(const A& a, ReadLock rhs_lk)
: field1_(a.field1_)
, field2_(a.field2_)
{}
mutexes
クラスタイプに入れることは「1つの本当の方法」ではありません。これはツールボックス内のツールであり、使用する場合はこの方法です。
これは逆さまの答えです。タイプのベースとして「このオブジェクトを同期する必要があります」を埋め込む代わりに、任意のタイプの下に挿入します。
同期されたオブジェクトの扱いは非常に異なります。大きな問題の1つは、デッドロック(複数のオブジェクトのロック)について心配する必要があることです。また、基本的に「オブジェクトのデフォルトバージョン」であってはなりません。同期されたオブジェクトは、競合するオブジェクト用であり、スレッド間の競合を最小限に抑えることを目標にする必要があります。
ただし、オブジェクトの同期は引き続き役立ちます。シンクロナイザーから継承する代わりに、任意の型を同期でラップするクラスを作成できます。ユーザーは、オブジェクトが同期されたので、オブジェクトに対して操作を実行するためにいくつかのフープをジャンプする必要がありますが、オブジェクトに対するいくつかの手動でコーディングされた制限された操作のセットに制限されません。オブジェクトに対する複数の操作を1つに構成することも、複数のオブジェクトに対する操作を行うこともできます。
これは、任意の型の同期ラッパーT
です。
template<class T>
struct synchronized {
template<class F>
auto read(F&& f) const&->std::result_of_t<F(T const&)> {
return access(std::forward<F>(f), *this);
}
template<class F>
auto read(F&& f) &&->std::result_of_t<F(T&&)> {
return access(std::forward<F>(f), std::move(*this));
}
template<class F>
auto write(F&& f)->std::result_of_t<F(T&)> {
return access(std::forward<F>(f), *this);
}
// uses `const` ness of Syncs to determine access:
template<class F, class... Syncs>
friend auto access( F&& f, Syncs&&... syncs )->
std::result_of_t< F(decltype(std::forward<Syncs>(syncs).t)...) >
{
return access2( std::index_sequence_for<Syncs...>{}, std::forward<F>(f), std::forward<Syncs>(syncs)... );
};
synchronized(synchronized const& o):t(o.read([](T const&o){return o;})){}
synchronized(synchronized && o):t(std::move(o).read([](T&&o){return std::move(o);})){}
// special member functions:
synchronized( T & o ):t(o) {}
synchronized( T const& o ):t(o) {}
synchronized( T && o ):t(std::move(o)) {}
synchronized( T const&& o ):t(std::move(o)) {}
synchronized& operator=(T const& o) {
write([&](T& t){
t=o;
});
return *this;
}
synchronized& operator=(T && o) {
write([&](T& t){
t=std::move(o);
});
return *this;
}
private:
template<class X, class S>
static auto smart_lock(S const& s) {
return std::shared_lock< std::shared_timed_mutex >(s.m, X{});
}
template<class X, class S>
static auto smart_lock(S& s) {
return std::unique_lock< std::shared_timed_mutex >(s.m, X{});
}
template<class L>
static void lock(L& lockable) {
lockable.lock();
}
template<class...Ls>
static void lock(Ls&... lockable) {
std::lock( lockable... );
}
template<size_t...Is, class F, class...Syncs>
friend auto access2( std::index_sequence<Is...>, F&&f, Syncs&&...syncs)->
std::result_of_t< F(decltype(std::forward<Syncs>(syncs).t)...) >
{
auto locks = std::make_tuple( smart_lock<std::defer_lock_t>(syncs)... );
lock( std::get<Is>(locks)... );
return std::forward<F>(f)(std::forward<Syncs>(syncs).t ...);
}
mutable std::shared_timed_mutex m;
T t;
};
template<class T>
synchronized< T > sync( T&& t ) {
return {std::forward<T>(t)};
}
C ++ 14およびC ++ 1z機能が含まれています。
これは、const
操作がマルチリーダーセーフであることをstd
前提としています(これはコンテナーが想定していることです)。
使用方法は次のようになります。
synchronized<int> x = 7;
x.read([&](auto&& v){
std::cout << v << '\n';
});
以下のためのint
同期アクセス権を持ちます。
持ってはいけないことをお勧めしsynchronized(synchronized const&)
ます。それが必要になることはめったにありません。
あなたが必要な場合はsynchronized(synchronized const&)
、私が交換するために誘惑されるだろうT t;
とstd::aligned_storage
、手動で配置を構築し、手動で破棄。これにより、適切なライフタイム管理が可能になります。
それを除けば、ソースをコピーしてT
、そこから読み取ることができます。
synchronized(synchronized const& o):
t(o.read(
[](T const&o){return o;})
)
{}
synchronized(synchronized && o):
t(std::move(o).read(
[](T&&o){return std::move(o);})
)
{}
割り当ての場合:
synchronized& operator=(synchronized const& o) {
access([](T& lhs, T const& rhs){
lhs = rhs;
}, *this, o);
return *this;
}
synchronized& operator=(synchronized && o) {
access([](T& lhs, T&& rhs){
lhs = std::move(rhs);
}, *this, std::move(o));
return *this;
}
friend void swap(synchronized& lhs, synchronized& rhs) {
access([](T& lhs, T& rhs){
using std::swap;
swap(lhs, rhs);
}, *this, o);
}
配置と整列されたストレージのバージョンは少し面倒です。いくつかのフープをジャンプする必要がある構築時を除いt
て、へのほとんどのアクセスはメンバー関数T&t()
とに置き換えられT const&t()const
ます。
synchronized
クラスの一部ではなくラッパーを作成することで、クラスが内部const
的にマルチリーダーであることを尊重し、シングルスレッドで書き込むことだけを保証する必要があります。
では珍しいです、我々は同期のインスタンスを必要とする場合、我々は、上記のようなフープを介してジャンプ。
上記のタイプミスについてお詫びします。おそらくいくつかあります。
上記の副次的な利点は、synchronized
(同じタイプの)オブジェクトに対するn項の任意の操作が、事前にハードコーディングすることなく連携して機能することです。フレンド宣言を追加すると、synchronized
複数のタイプのn項オブジェクトが連携して機能する可能性があります。access
その場合、過負荷の競合に対処するために、インラインの友達から離れなければならないかもしれません。
ミューテックスとC ++移動セマンティクスを使用することは、スレッド間でデータを安全かつ効率的に転送するための優れた方法です。
文字列のバッチを作成し、それらを(1つ以上の)コンシューマーに提供する「プロデューサー」スレッドを想像してみてください。これらのバッチは、(潜在的に大きな)オブジェクトを含むオブジェクトで表すことができstd::vector<std::string>
ます。これらのベクトルの内部状態を、不要な重複なしにコンシューマーに「移動」したいのです。
ミューテックスは、オブジェクトの状態の一部ではなく、オブジェクトの一部として認識されるだけです。つまり、ミューテックスを移動したくないということです。
必要なロックは、アルゴリズム、オブジェクトの一般化の程度、および許可する使用範囲によって異なります。
あなただけの今までのオブジェクト「を消費する」スレッドローカルに共有状態「プロデューサー」オブジェクトから移動した場合、あなたは唯一の移動ロックにOKかもしれませんから場合オブジェクトます。
より一般的な設計の場合は、両方をロックする必要があります。このような場合は、デッドロックを検討する必要があります。
それが潜在的な問題である場合は、std::lock()
デッドロックのない方法で両方のミューテックスのロックを取得するために使用します。
http://en.cppreference.com/w/cpp/thread/lock
最後に、移動のセマンティクスを確実に理解する必要があります。移動したオブジェクトが有効であるが不明な状態のままになっていることを思い出してください。移動を実行していないスレッドが、有効であるが不明な状態を検出した場合に、移動元のオブジェクトにアクセスしようとする正当な理由がある可能性は十分にあります。
繰り返しになりますが、私のプロデューサーは文字列を叩き出しているだけで、コンシューマーはすべての負荷を取り除いています。その場合、プロデューサーがベクターに追加しようとするたびに、ベクターが空でないか空であることがわかります。
つまり、移動されたオブジェクトへの潜在的な同時アクセスが書き込みに相当する場合は、問題がない可能性があります。読み取りに相当する場合は、任意の状態を読み取っても問題がない理由を考えてください。
まず、ミューテックスを含むオブジェクトを移動する場合は、デザインに問題があるはずです。
ただし、とにかくそれを行うことにした場合は、moveコンストラクターで新しいミューテックスを作成する必要があります。
// movable
struct B{};
class A {
B b;
std::mutex m;
public:
A(A&& a)
: b(std::move(a.b))
// m is default-initialized.
{
}
};
これはスレッドセーフです。moveコンストラクターは、引数が他の場所で使用されていないと安全に想定できるため、引数をロックする必要がありません。
A a; A a2(std::move(a)); do some stuff with a
。
new
ある場合は、インスタンスを作成して配置することをお勧めします。これは、std::unique_ptr
よりクリーンに見え、混乱の問題につながる可能性は低いです。良い質問。