54

私は何人かの人々が嫌いなのを見てきましたrecursive_mutex:

http://www.zaval.org/resources/library/butenhof1.html

しかし、スレッド セーフ (mutex 保護) であるクラスを実装する方法を考えると、mutex で保護する必要があるすべてのメソッドが mutex で保護され、mutex が最大 1 回ロックされることを証明するのは非常に難しいように思えます。

したがって、オブジェクト指向設計では、(1 つのリソースのみを保護するために) 1 か所でのみ使用されない限り、std::recursive_mutexデフォルトであり、一般的なケースでパフォーマンスの最適化と見なされる必要がありますか?std::mutex

明確にするために、1 つの非静的ミューテックスについて話しています。したがって、各クラス インスタンスにはミューテックスが 1 つしかありません。

各パブリック メソッドの開始時:

{
    std::scoped_lock<std::recursive_mutex> sl;
4

3 に答える 3

93

ほとんどの場合、再帰的ミューテックスが必要だと思うなら、あなたの設計は間違っているので、それをデフォルトにするべきではありません。

データ メンバーを保護する単一のミューテックスを持つクラスの場合、ミューテックスはすべてのメンバー関数でロックされ、すべてのpublicメンバー関数privateはミューテックスが既にロックされていると想定する必要があります。

publicメンバー関数が別のメンバー関数を呼び出す必要がある場合はpublic、2 番目の関数を 2 つに分割しますprivate。作業を行う実装関数とpublic、mutex をロックしてその関数を呼び出すだけのメンバー関数ですprivate。最初のメンバー関数は、再帰的なロックを気にせずに実装関数を呼び出すこともできます。

例えば

class X {
    std::mutex m;
    int data;
    int const max=50;

    void increment_data() {
        if (data >= max)
            throw std::runtime_error("too big");
        ++data;
    }
public:
    X():data(0){}
    int fetch_count() {
        std::lock_guard<std::mutex> guard(m);
        return data;
    }
    void increase_count() {
        std::lock_guard<std::mutex> guard(m);
        increment_data();
    } 
    int increase_count_and_return() {
        std::lock_guard<std::mutex> guard(m);
        increment_data();
        return data;
    } 
};

もちろん、これは簡単な例ですが、increment_data関数は 2 つのパブリック メンバー関数間で共有され、それぞれがミューテックスをロックします。シングルスレッド コードでは、 にインライン化してincrease_count、それincrease_count_and_returnを呼び出すことができますが、マルチスレッド コードではそれを行うことはできません。

これは、優れた設計原則の単なる適用です。パブリック メンバー関数はミューテックスをロックする責任を負い、作業を実行する責任をプライベート メンバー関数に委任します。

これには、クラスが一貫した状態にあるときにのみメンバー関数が呼び出されなければならないという利点がありpublicます。ミューテックスはロック解除され、ロックされるとすべての不変条件が保持されます。publicメンバー関数を相互に呼び出す場合、ミューテックスが既にロックされていて、不変条件が必ずしも保持されていない場合を処理する必要があります。

また、条件変数の待機などが機能することも意味します。再帰的ミューテックスのロックを条件変数に渡すと、(a) 機能しないstd::condition_variable_anyため使用する必要がありstd::condition_variable、(b) 1 レベルのロックのみが解放されます。 、したがって、述語をトリガーして通知を実行するスレッドがロックを取得できないため、ロックを保持している可能性があり、デッドロックが発生する可能性があります。

再帰的ミューテックスが必要なシナリオを考えるのに苦労しています。

于 2013-01-30T09:46:33.337 に答える
27

std::recursive_mutexデフォルトでありstd::mutex、パフォーマンスの最適化と見なす必要がありますか?

そうではありません。非再帰ロックを使用する利点は、パフォーマンスの最適化だけではありません。コードが、リーフレベルのアトミック操作が実際にはリーフレベルであることを自己チェックしていることを意味し、ロックを使用する他の何かを呼び出していません。

あなたが持っているかなり一般的な状況があります:

  • シリアル化が必要な操作を実装する関数で、mutex を使用して実行します。
  • より大きなシリアル化された操作を実装し、最初の関数を呼び出してその 1 つのステップを実行しようとしている別の関数が、より大きな操作のロックを保持している場合。

具体的な例として、おそらく最初の関数はリストからノードをアトミ​​ックに削除し、2 番目の関数はリストから 2 つのノードをアトミ​​ックに削除ます (2 つのノードのうち 1 つだけが取得されたリストを別のスレッドに表示させたくありません)。アウト)。

これには再帰的ミューテックスは必要ありません。たとえば、最初の関数をパブリック関数としてリファクタリングして、ロックを取得し、「安全でない」操作を行うプライベート関数を呼び出すことができます。2 番目の関数は、同じプライベート関数を呼び出すことができます。

ただし、代わりに再帰的ミューテックスを使用すると便利な場合があります。この設計にはまだ問題があります:クラスの不変条件が保持されないポイントでのremove_two_nodes呼び出しremove_one_node(2 回目の呼び出しでは、リストは正確に公開したくない状態になります)。remove_one_nodeしかし、 がその不変条件に依存していないことがわかっていると仮定すると、これは設計上の重大な欠陥ではなく、理想よりも少し複雑なルールを作成しただけです。入った」。

したがって、このトリックは時々役に立ちます。私はこの記事ほど、再帰的ミューテックスを嫌いではありません。私は、Posix にそれらを含める理由が、「mutex 属性とスレッド拡張機能を示すため」という記事の内容とは異なると主張する歴史的知識を持っていません。ただし、私は確かにそれらをデフォルトとは考えていません。

設計で再帰ロックが必要かどうかわからない場合は、設計が不完全であると言っても過言ではありません。コードを書いていて、ロックがすでに保持されているかどうかなど、基本的に重要なことを知らないという事実を後で後悔するでしょう。したがって、「万が一に備えて」再帰ロックを入れないでください。

必要であることがわかっている場合は、それを使用してください。必要がないことがわかっている場合、非再帰ロックを使用することは単なる最適化ではなく、設計の制約を強化するのに役立ちます。2 番目のロックが失敗するほうが、ロックが成功して、設計上絶対に発生してはならないことを誤って実行してしまったという事実を隠すよりも便利です。しかし、自分の設計に従い、ミューテックスを二重にロックしないと、それが再帰的であるかどうかを知ることができず、再帰的ミューテックスは直接害を及ぼすことはありません。

この類推は失敗するかもしれませんが、別の見方をすると次のようになります。2 種類のポインターから選択したとします。1 つはヌル ポインターを逆参照するときにスタック トレースでプログラムを中止し、もう 1 つは戻ります0(またはそれをより多くの型に拡張する: ポインターが値を参照しているかのように動作します)。初期化されたオブジェクト)。非再帰的ミューテックスはアボートするものに少し似ており、再帰的ミューテックスは 0 を返すものに少し似ています。どちらにも用途がある可能性があります。 -value"値。ただし、ヌル ポインターを逆参照しないようにコードが設計されている場合は、それを黙って許可するバージョンを既定で使用したくありません。

于 2013-01-24T10:35:58.937 に答える