C ++の可動型のミューテックスをどのように処理する必要がありますか?


86

設計上、std::mutex移動もコピーもできません。これはA、ミューテックスを保持するクラスがデフォルトの移動コンストラクターを受け取らないことを意味します。

このタイプをAスレッドセーフな方法で移動可能にするにはどうすればよいですか?


4
質問には癖があります。移動操作自体もスレッドセーフである必要がありますか、それともオブジェクトへの他のアクセスがスレッドセーフである場合は十分ですか?
ジョナス・シェーファー

2
@paulmそれは本当にデザインに依存します。クラスにミューテックスメンバー変数があるのをよく見ましたが、その場合std::lock_guardはメソッドスコープのみです。
Cory Kramer 2015年

2
@Jonas Wielicki:最初は、移動もスレッドセーフである必要があると思いました。ただし、もう一度考えてみると、これはあまり意味がありません。オブジェクトを移動構築すると、通常、古いオブジェクトの状態が無効になるためです。したがって、他のスレッド、古いオブジェクトを移動する場合は、そのオブジェクトにアクセスできないようにする必要があります。そうしないと、すぐに無効なオブジェクトにアクセスする可能性があります。私は正しいですか?
ジャックサバス2015年

2
このリンクをたどってください。justsoftwaresolutions.co.uk/ threading /…
Ravi Chauhan

1
@DieterLücking:ええ、これがアイデアです。ミューテックスMはクラスBを保護します。ただし、スレッドセーフでアクセス可能なオブジェクトを作成するために両方をどこに保存しますか?MとBの両方がクラスAに移動する可能性があります。この場合、クラスAはクラススコープにミューテックスを持ちます。
ジャックサバス2015年

回答:


105

少しのコードから始めましょう:

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_mutexforを使用してセルフチェックなしで解決することもでき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_)
    {}

2
コピーコンストラクターはフィールドを割り当てますが、フィールドはコピーしません。つまり、デフォルトで構築可能である必要がありますが、これは残念な制限です。
Yakk-Adam Nevraumont 2015年

@Yakk:はい、mutexesクラスタイプに入れることは「1つの本当の方法」ではありません。これはツールボックス内のツールであり、使用する場合はこの方法です。
ハワードヒナント2015年

@Yakk:私の答えで文字列「C ++ 14」を検索してください。
ハワードヒナント2015年

ああ、すみません、私はそのC ++ 14ビットを逃しました。
Yakk-Adam Nevraumont 2015年

2
素晴らしい説明@HowardHinnant!C ++ 17では、std :: scoped_lock lock(x.mut_、y_mut_);を使用することもできます。そうすれば、実装に依存して、いくつかのミューテックスを適切な順序でロックできます
fen

7

これに答えるのに良い、きれいで、簡単な方法がないように思われることを考えると、アントンの解決策は正しいと思いますが、より良い答えが出ない限り、そのようなクラスをヒープに置いて世話をすることをお勧めします経由std::unique_ptr

auto a = std::make_unique<A>();

今では完全に移動可能なタイプであり、移動中に内部ミューテックスをロックしている人は、これが良いことかどうかについて議論があったとしても、安全です。

コピーセマンティクスが必要な場合は、

auto a2 = std::make_shared<A>();

5

これは逆さまの答えです。タイプのベースとして「このオブジェクトを同期する必要があります」を埋め込む代わりに、任意のタイプのに挿入します。

同期されたオブジェクトの扱いは非常に異なります。大きな問題の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その場合、過負荷の競合に対処するために、インラインの友達から離れなければならないかもしれません。

実例


4

ミューテックスとC ++移動セマンティクスを使用することは、スレッド間でデータを安全かつ効率的に転送するための優れた方法です。

文字列のバッチを作成し、それらを(1つ以上の)コンシューマーに提供する「プロデューサー」スレッドを想像してみてください。これらのバッチは、(潜在的に大きな)オブジェクトを含むオブジェクトで表すことができstd::vector<std::string>ます。これらのベクトルの内部状態を、不要な重複なしにコンシューマーに「移動」したいのです。

ミューテックスは、オブジェクトの状態の一部ではなく、オブジェクトの一部として認識されるだけです。つまり、ミューテックスを移動したくないということです。

必要なロックは、アルゴリズム、オブジェクトの一般化の程度、および許可する使用範囲によって異なります。

あなただけの今までのオブジェクト「を消費する」スレッドローカルに共有状態「プロデューサー」オブジェクトから移動した場合、あなたは唯一の移動ロックにOKかもしれませんから場合オブジェクトます。

より一般的な設計の場合は、両方をロックする必要があります。このような場合は、デッドロックを検討する必要があります。

それが潜在的な問題である場合は、std::lock()デッドロックのない方法で両方のミューテックスのロックを取得するために使用します。

http://en.cppreference.com/w/cpp/thread/lock

最後に、移動のセマンティクスを確実に理解する必要があります。移動したオブジェクトが有効であるが不明な状態のままになっていることを思い出してください。移動を実行していないスレッドが、有効であるが不明な状態を検出した場合に、移動元のオブジェクトにアクセスしようとする正当な理由がある可能性は十分にあります。

繰り返しになりますが、私のプロデューサーは文字列を叩き出しているだけで、コンシューマーはすべての負荷を取り除いています。その場合、プロデューサーがベクターに追加しようとするたびに、ベクターが空でないか空であることがわかります。

つまり、移動されたオブジェクトへの潜在的な同時アクセスが書き込みに相当する場合は、問題がない可能性があります。読み取りに相当する場合は、任意の状態を読み取っても問題がない理由を考えてください。


3

まず、ミューテックスを含むオブジェクトを移動する場合は、デザインに問題があるはずです。

ただし、とにかくそれを行うことにした場合は、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コンストラクターは、引数が他の場所で使用されていないと安全に想定できるため、引数をロックする必要がありません。


2
それはスレッドセーフではありません。a.mutexがロックされている場合はどうなりますか:その状態を失います。-1

2
@DieterLücking引数が移動元オブジェクトへの唯一の参照である限り、そのミューテックスがロックされる正当な理由はありません。たとえそうだとしても、新しく作成されたオブジェクトのミューテックスをロックする理由はありません。そして、もしあれば、これはミューテックスを備えた可動オブジェクトの全体的な悪い設計の議論です。
アントン

1
@DieterLückingこれは真実ではありません。問題を説明するコードを提供できますか?そして、形式ではありませんA a; A a2(std::move(a)); do some stuff with a
アントンサヴィン2015年

2
ただし、これがとにかく最善の方法でnewある場合は、インスタンスを作成して配置することをお勧めします。これは、std::unique_ptrよりクリーンに見え、混乱の問題につながる可能性は低いです。良い質問。
マイクバイン2015年

1
@MikeVine答えとして追加する必要があると思います。
アントンサヴィン2015年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.