web-dev-qa-db-ja.com

C ++で移動可能な型のミューテックスをどのように扱うべきですか?

意図的に、 std::mutexは、移動もコピー構築もできません。これは、ミューテックスを保持するクラスAがdefault-move-constructorを受け取らないことを意味します。

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

74
Jack Sabbath

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

_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でははるかに役立つように、かなり示唆的なタイプエイリアスをそこに入れました。しばらくお待ちください。

あなたの質問は次のように要約されます:

このクラスの移動コンストラクターと移動代入演算子を作成するにはどうすればよいですか?

移動コンストラクターから始めます。

移動コンストラクター

メンバーmutexmutableになっていることに注意してください。厳密に言えば、これはムーブメンバーには必要ありませんが、コピーメンバーも必要だと思います。そうでない場合は、ミューテックス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;
    }
_

2つのミューテックスを次々にロックするのではなく、std::lock(m1, m2)を使用してロックする必要があることに注意してください。それらを次々にロックすると、2つのスレッドが上記のように2つのオブジェクトを反対の順序で割り当てると、デッドロックが発生する可能性があります。 _std::lock_のポイントは、そのデッドロックを回避することです。

コンストラクタのコピー

あなたはコピーのメンバーについては尋ねませんでしたが、今すぐそれらについて話すかもしれません(あなたでなければ、誰かがそれらを必要とします)。

_    A(const A& a)
    {
        ReadLock  rhs_lk(a.mut_);
        field1_ = a.field1_;
        field2_ = a.field2_;
    }
_

コピーコンストラクタは、ReadLockの代わりにWriteLockエイリアスが使用されることを除いて、moveコンストラクタによく似ています。現在、これらは両方とも別名_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>;
_

これは最適化かもしれませんが、間違いではありません。そうであるかどうかを判断するために測定する必要があります。しかし、この変更により、コンストラクトfromを複数のスレッドの同じ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について考えることで、「スレッドセーフ」Aを提供する必要があるかもしれないAPIの洞察を得ることができます。これは一般に「非スレッドセーフ」とは異なります「ロックの粒度」の問題のためのAPI。

また、「セルフスワップ」から保護する必要があることに注意してください。 「セルフスワップ」は無操作である必要があります。セルフチェックがなければ、同じミューテックスを再帰的にロックします。これは、MutexTypeに_std::recursive_mutex_を使用することにより、自己チェックなしでも解決できます。

更新

以下のコメントでは、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();
    }
    _

この更新を完了する前に、ヤックはコメントを消しました。しかし、彼はこの問題を推し進め、この答えに解決策を見出した功績に値します。

更新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_)
    {}
_
92
Howard Hinnant

これに答えるニース、クリーン、簡単な方法はないと思われる-アントンのソリューションI 思考は正しいが、間違いなく議論の余地があり、より良い答えが出ない限り、そのようなクラスを置くことをお勧めしますヒープ上で、std::unique_ptr

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

現在は完全に移動可能なタイプであり、内部ミューテックスにロックがあり、移動が発生している人は誰でも安全です。

コピーのセマンティクスが必要な場合は、単に使用します

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

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

同期オブジェクトの処理方法は非常に異なります。大きな問題の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をマルチリーダーであると見なし、それをシングルスレッド方式で記述することを保証する必要があります。

rareの場合、同期されたインスタンスが必要なため、上記のようなフープをジャンプします。

上記のタイプミスをおologiesびします。おそらくいくつかあります。

上記の副次的な利点は、(同じ型の)synchronizedオブジェクトに対する任意のn項演算が、事前にハードコーディングすることなく、一緒に機能することです。フレンド宣言を追加すると、複数のタイプのn-ary synchronizedオブジェクトが一緒に機能する場合があります。その場合、accessをインラインフレンドから移動して、過負荷の競合に対処する必要があります。

実例

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

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

ミューテックスは、オブジェクトの状態の一部ではなく、オブジェクトの一部として単に認識されます。つまり、ミューテックスを移動する必要はありません。

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

共有状態の「プロデューサー」オブジェクトからスレッドローカルの「消費」オブジェクトにのみ移動する場合は、移動したfromのみをロックしてもかまいませんオブジェクト。

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

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

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

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

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

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

4
Persixty

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

しかし、とにかくそれを行うことにした場合、ムーブコンストラクターで新しいミューテックスを作成する必要があります。

// movable
struct B{};

class A {
    B b;
    std::mutex m;
public:
    A(A&& a)
        : b(std::move(a.b))
        // m is default-initialized.
    {
    }
};

これはスレッドセーフです。ムーブコンストラクターは、引数が他のどこでも使用されていないと安全に想定できるため、引数のロックは必要ありません。

3
Anton Savin