1. ホーム
  2. c++

[解決済み] C++でmovable型のmutexをどのように扱えばいいですか?

2023-01-04 17:26:01

質問

デザインで std::mutex は移動もコピーもできません。これは、クラス A はデフォルトの移動コンストラクタを受け取らないということです。

どのようにしてこの型を A をスレッドセーフな方法で移動可能にするにはどうしたらよいでしょうか?

どのように解決するのですか?

まずは、ちょっとしたコードから。

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 . 厳密に言えば、これは移動メンバには必要ないのですが、コピーメンバも必要なのではと推測されます。 そうでない場合は、mutex を作る必要はありません。 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) を使ってロックしなければならないことに注意してください。 もし、1つずつロックしてしまうと、2つのスレッドが上記のように2つのオブジェクトを逆の順序で割り当てたときに、デッドロックが発生する可能性があります。 のポイントは std::lock のポイントは、そのデッドロックを回避することです。

コピーコンストラクタ

あなたはコピーメンバーについて尋ねませんでしたが、今話してもよいかもしれません(あなたでなくても、誰かがそれを必要とするでしょう)。

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

copy コンストラクタは move コンストラクタと似ていますが 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 について考えることは、quot;スレッドセーフのために提供する必要があるかもしれない API についての洞察を与えることができます。 A これは一般的に、quot;ロック粒度の問題から、quot;スレッドセーフでないAPIとは異なるでしょう。

また、"self-swap" から保護する必要性にも注意してください。 "self-swap" は禁止されるべきものです。 自己チェックがなければ、同じミューテックスを再帰的にロックすることになります。 これはまた、セルフチェックなしで std::recursive_mutex を使用することで、セルフチェックなしで解決することもできます。 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_)
    {}