1. ホーム
  2. c++

[解決済み] condition_variable.notify_one()を呼び出す前にロックを取得する必要がありますか?

2023-02-05 13:10:28

質問

の使い方について少し混乱しています。 std::condition_variable . 私は unique_lock の上に mutex を呼び出す前に condition_variable.wait() . を呼び出す前にユニークロックを取得する必要があるかどうかについては、見つけることができませんでした。 notify_one() または notify_all() .

の例 cppreference.com は矛盾しています。例えば notify_one page はこのような例をあげています。

#include <iostream>
#include <condition_variable>
#include <thread>
#include <chrono>

std::condition_variable cv;
std::mutex cv_m;
int i = 0;
bool done = false;

void waits()
{
    std::unique_lock<std::mutex> lk(cv_m);
    std::cout << "Waiting... \n";
    cv.wait(lk, []{return i == 1;});
    std::cout << "...finished waiting. i == 1\n";
    done = true;
}

void signals()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Notifying...\n";
    cv.notify_one();

    std::unique_lock<std::mutex> lk(cv_m);
    i = 1;
    while (!done) {
        lk.unlock();
        std::this_thread::sleep_for(std::chrono::seconds(1));
        lk.lock();
        std::cerr << "Notifying again...\n";
        cv.notify_one();
    }
}

int main()
{
    std::thread t1(waits), t2(signals);
    t1.join(); t2.join();
}

ここでは、ロックは最初の notify_one() にはかかりませんが、2つ目の notify_one() . 他のページの例を見てみると、さまざまなことがわかりますが、ほとんどはロックを取得していません。

  • を呼び出す前に、自分自身で mutex をロックすることを選択することはできますか? notify_one() を呼び出す前にミューテックスをロックすることを自分で選択できますか、そしてなぜそれをロックすることを選択するのでしょうか?
  • 与えられた例で、なぜ最初の notify_one() にはロックがなく、それ以降の呼び出しにはロックがあるのはなぜでしょうか。この例は間違っているのでしょうか、それとも何か根拠があるのでしょうか。

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

を呼び出す際に、ロックを保持する必要はありません。 condition_variable::notify_one() を呼び出すときにロックを保持する必要はありませんが、それはまだよく定義された動作であり、エラーではないという意味で間違いではありません。

しかし、待機中のスレッドが実行可能になれば (もしあれば) すぐに通知スレッドが保持するロックを取得しようとするので、これは "pessimization" であるかもしれません。を呼び出している間は、条件変数に関連付けられたロックを保持しないようにするのが良い経験則になると思います。 notify_one() または notify_all() . 参照 Pthread Mutex: pthread_mutex_unlock() は多くの時間を消費します。 の pthread 相当のものを呼ぶ前にロックを解放する例については、pthread_mutex_unlock() を参照してください。 notify_one() を呼び出す前にロックを解放することで、パフォーマンスが大幅に改善されました。

を覚えておいてください。 lock() の呼び出しは while ループの間にロックを保持する必要があるためです。 while (!done) ループの条件チェックの間、ロックを保持する必要があるからです。というのは、ループの条件チェックの間はロックを保持する必要があるからです。 notify_one() .


2016-02-27 : レースコンディションが存在するかどうかについてのコメントでいくつかの質問に対処するための大規模な更新は、ロックは、そのためのヘルプではありません。 notify_one() を呼び出します。この質問は 2 年近く前にされたものなので、この更新は遅いと思いますが、@Cookie さんの質問である、プロデューサー ( signals() この例では) が notify_one() の直前にコンシューマ( waits() この例では) が wait() .

重要なのは i - というオブジェクトがどうなるかということです。 このオブジェクトは condition_variable への変更を効率的に待つためのメカニズムに過ぎません。 i .

を更新するとき、プロデューサーはロックを保持する必要があります。 i をチェックするとき、コンシューマはロックを保持する必要があります。 i をチェックし condition_variable::wait() (を呼び出すことです(もし待つ必要があるのなら)。この場合、重要なのは ロックを保持しているのと同じインスタンスでなければならないということです。 (しばしばクリティカル・セクションと呼ばれます) であることです。クリティカルセクションはプロデューサーが更新するときに保持されるので i を更新したときと、コンシューマがこのセクションをチェック&ウェイトするときです。 i をチェック&ウェイしているときは i をチェックする間に変化することはありません。 i をチェックするときと condition_variable::wait() . これが条件変数を正しく使うためのポイントです。

C++標準では、(今回のように)述語で呼ばれた場合、condition_variable::wait()は以下のような挙動を示すとされています。

while (!pred())
    wait(lock);

コンシューマがチェックする際に発生しうる状況は2つあります。 i :

  • もし i が 0 ならば、コンシューマは cv.wait() を呼び出します。 i が 0 のままであれば wait(lock) の部分が呼び出されたときにも、0 のままです。 この場合、プロデューサには condition_variable::notify_one() を呼び出す機会がありません。 while を呼び出した後までループします。 cv.wait(lk, []{return i == 1;}) (そして wait() の呼び出しは、notify を適切に「捕捉」するために必要なことをすべて行っています -。 wait() はそれが完了するまでロックを解放しません)。 したがって、この場合、消費者は通知を見逃すことはできません。

  • もし i がすでに 1 である場合、コンシューマは cv.wait() を呼び出したとき、その wait(lock) の部分は決して呼び出されないので、実装の while (!pred()) テストが内部ループを終了させるためです。 この状況では、notify_one() の呼び出しがいつ発生するかは問題ではなく、コンシューマはブロックされません。

この例では、さらに複雑な要素として done 変数を使ってプロデューサー・スレッドにシグナルを送り、 コンシューマーが i == 1 へのアクセスは、すべて done へのアクセス (読み込みと修正の両方) は、同じ重要なセクションで icondition_variable .

@eh9さんが指摘された質問を見ると。 std::atomic と std::condition_variable を使用すると同期が信頼できない を見ると、あなたは はレースコンディションを参照してください。しかし、その質問で投稿されたコードは、条件変数を使用する際の基本的なルールの1つを破っています。 それは、チェック アンド ウェイトを実行するときに、単一のクリティカル セクションを保持しないことです。

その例では、コードは次のようになります。

if (--f->counter == 0)      // (1)
    // we have zeroed this fence's counter, wake up everyone that waits
    f->resume.notify_all(); // (2)
else
{
    unique_lock<mutex> lock(f->resume_mutex);
    f->resume.wait(lock);   // (3)
}

このとき wait() を押しながら、#3 の f->resume_mutex . しかし、そのチェックは wait() が必要であるかどうかのチェックは、ステップ#1 で ではなく は、そのロックを保持している間は全く行われません(ましてや、チェックアンドウェイトのために継続的に行われることはありません)。 そのコードスニペットで問題を抱えている人は、以下のように考えたのだと思います。 f->counterstd::atomic 型であれば、要件を満たすことができます。しかし std::atomic が提供するアトミック性は、その後に呼ばれる f->resume.wait(lock) . この例では、以下のような競合が発生しています。 f->counter がチェックされたとき (ステップ #1) と wait() が呼び出されたとき(ステップ#3)。

この質問の例では、そのレースは存在しません。