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


90

の使用について少し混乱していますstd::condition_variable。を呼び出す前unique_lockに、を作成する必要があることを理解しています。私は何を見つけることができない、私はまた、呼び出す前に独自のロックを取得する必要があるかどうかですか。mutexcondition_variable.wait()notify_one()notify_all()

cppreference.comの例は矛盾しています。たとえば、notify_oneページは次の例を示しています。

#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()ます。例のある他のページを見ると、ほとんどの場合ロックを取得していない、さまざまなことがわかります。

  • 呼び出す前にミューテックスをロックすることを自分で選択できますnotify_one()か?なぜそれをロックすることを選択するのですか?
  • 与えられた例では、なぜ最初のnotify_one()呼び出しにはロックがないのに、後続の呼び出しにはロックがあるのですか。この例は間違っていますか、それともいくつかの理由がありますか?

回答:


77

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

ただし、待機中のスレッドが実行可能になった場合(存在する場合)は、通知スレッドが保持しているロックをすぐに取得しようとするため、「ペシミゼーション」になる可能性があります。notify_one()またはを呼び出すときに条件変数に関連付けられたロックを保持しないようにすることは、経験則として適切だと思いますnotify_all()Pthread Mutexを参照してください。pthread_mutex_unlock()はnotify_one()パフォーマンスの向上に相当するpthreadを呼び出す前にロックを解放する例について、多くの時間消費します

ループ状態のチェック中にロックを保持する必要があるため、ある時点でループ内のlock()呼び出しがwhile必要になることに注意してwhile (!done)ください。ただし、への呼び出しのために保持する必要はありませんnotify_one()


2016-02-27:競合状態があるかどうかについてのコメントのいくつかの質問に対処するための大規模な更新は、ロックがnotify_one()通話に役立たないことです。質問がほぼ2年前に行われたため、この更新が遅れていることはわかっていますが、プロデューサー(signals()この例)がnotify_one()コンシューマー(waits()この例)の直前に電話をかけた場合の競合状態の可能性に関する@Cookieの質問に対処したいと思います。呼び出すことができますwait()

重要なのは何が起こるiかです。これは、消費者が「仕事」をする必要があるかどうかを実際に示すオブジェクトです。これcondition_variableは、消費者がへの変更を効率的に待機できるようにするための単なるメカニズムiです。

プロデューサーは更新時にロックを保持する必要がiあり、コンシューマーはチェックiおよび呼び出し中にロックを保持する必要がありますcondition_variable::wait()(待機する必要がある場合)。この場合、重要なのは、コンシューマーがこのチェックアンドウェイトを実行するときに、ロックを保持するのと同じインスタンス(クリティカルセクションと呼ばれることが多い)でなければならないということです。クリティカルセクションは、プロデューサーが更新するiときとコンシューマーがチェックアンドウェイトするときに保持されるため、コンシューマーがチェックするときと呼び出すときを変更するi機会はありません。これは、条件変数を適切に使用するための要点です。iicondition_variable::wait()

C ++標準では、(この場合のように)述語を指定して呼び出された場合、condition_variable :: wait()は次のように動作するとされています。

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

消費者がチェックするときに発生する可能性のある2つの状況がありますi

  • もしi0その後、消費者の呼び出しがされcv.wait()、その後、iときまだ0になりますwait(lock)実装の一部が呼び出された-ロックの適切な使用はそれを保証します。この場合、プロデューサーは、コンシューマーが呼び出しを行うまでcondition_variable::notify_one()whileループ内でを呼び出す機会がありませんcv.wait(lk, []{return i == 1;})(そして、wait()呼び出しは、通知を適切に「キャッチ」するために必要なすべてを実行します-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->counterは、std::atomicタイプなのでこれで要件を満たすと思ったと思います。ただし、によって提供されるアトミック性はstd::atomic、後続のf->resume.wait(lock)。への呼び出しには拡張されません。この例では、f->counterチェックされたとき(ステップ#1)とwait()呼び出されたとき(ステップ#3)の間に競合があります。

この質問の例には、その人種は存在しません。


2
それはより深い意味を持っています:domaigne.com/blog/computing/…特に、あなたが言及するpthreadの問題は、より新しいバージョンまたは正しいフラグで構築されたバージョンのいずれかによって解決されるべきです。(wait morphing最適化を有効にするため)このリンクで説明されている経験則:より予測可能な結果を​​得るには、スレッドが2つを超える状況ではWITHロックを通知する方が適切です。
v.oddou 2014年

6
@Michael:私の理解では、消費者は最終的にに電話する必要がありますthe_condition_variable.wait(lock);。プロデューサーとコンシューマーを同期するために必要なロックがない場合(たとえば、基になるのはロックのないspscキューです)、プロデューサーがロックしない限り、そのロックは目的を果たしません。私は元気です。しかし、まれなレースのリスクはありませんか?プロデューサーがロックを保持していない場合、コンシューマーが待機する直前にnotify_oneを呼び出すことはできませんか?その後、消費者は待機状態になり、目を覚ましません...
Cookie

1
たとえば、上記のコードでstd::cout << "Waiting... \n";、プロデューサーが実行している間にコンシューマーがいるとcv.notify_one();すると、ウェイクアップコールが失われます...または、ここで何かが欠落していますか?
Cookie

1
@クッキー。はい、そこには競合状態があります。stackoverflow.com/questions/20982270/を

1
@ eh9:くそー、コメントのおかげで時々コードがフリーズするバグの原因を見つけました。これは、この競合状態の正確なケースによるものでした。通知後にミューテックスのロックを解除すると、問題が完全に解決しました...どうもありがとうございました!
ガリネット2016

10

状況

vc10とBoost1.56を使用して、このブログ投稿が示唆しているように、並行キューを実装しました。作成者は、競合を最小限に抑えるためnotify_one()にミューテックスのロックを解除します。つまり、ミューテックスのロックを解除した状態で呼び出されます。

void push(const T& item)
{
  std::unique_lock<std::mutex> mlock(mutex_);
  queue_.push(item);
  mlock.unlock();     // unlock before notificiation to minimize mutex contention
  cond_.notify_one(); // notify one waiting thread
}

ミューテックスのロック解除は、Boostドキュメントの例に裏付けられています

void prepare_data_for_processing()
{
    retrieve_data();
    prepare_data();
    {
        boost::lock_guard<boost::mutex> lock(mut);
        data_ready=true;
    }
    cond.notify_one();
}

問題

それでも、これは次の不安定な動作につながりました。

  • まだ呼び出されnotify_one()いない間はcond_.wait()boost::thread::interrupt()
  • かつてnotify_one()は初めてcond_.wait()デッドロックが呼び出されました。待機は、boost::thread::interrupt()またはそれboost::condition_variable::notify_*()以上で終了することはできません。

解決

行を削除するとmlock.unlock()、コードは期待どおりに機能しました(通知と割り込みによって待機が終了します)。notify_one()ミューテックスがロックされたままで呼び出されることに注意してください。スコープを離れると、直後にロックが解除されます。

void push(const T& item)
{
  std::lock_guard<std::mutex> mlock(mutex_);
  queue_.push(item);
  cond_.notify_one(); // notify one waiting thread
}

つまり、少なくとも私の特定のスレッド実装ではboost::condition_variable::notify_one()、両方の方法が正しいように見えますが、呼び出す前にミューテックスのロックを解除してはなりません。


この問題をBoost.Threadに報告しましたか?私は、同様のタスクが存在見つけることができませんsvn.boost.org/trac/boost/...
magras

@magras悲しいことに、私はそうしませんでした。なぜ私がこれを考慮しなかったのか分かりません。残念ながら、前述のキューを使用してこのエラーを再現することはできません。
マテウスBrandlの

早期のウェイクアップがデッドロックを引き起こす可能性があるかどうかはわかりません。具体的には、push()がキューミューテックスを解放した後、notify_one()が呼び出される前にpop()でcond_.wait()から出た場合、Pop()はキューが空でないことを確認し、ではなく新しいエントリを消費する必要がありますwait()ing。push()がキューを更新しているときにcond_.wait()から出た場合、ロックはpush()によって保持される必要があるため、pop()はロックが解放されるのを待つことをブロックする必要があります。その他の早期ウェイクアップはロックを保持し、pop()が次のwait()を呼び出す前にpush()がキューを変更しないようにします。私は何を取りこぼしたか?
ケビン

4

他の人が指摘しているように、notify_one()競合状態やスレッド関連の問題の観点から、電話をかけるときにロックを保持する必要はありません。ただし、が呼び出されるcondition_variable前にnotify_one()が破壊されるのを防ぐために、ロックを保持する必要がある場合があります。次の例を考えてみましょう。

thread t;

void foo() {
    std::mutex m;
    std::condition_variable cv;
    bool done = false;

    t = std::thread([&]() {
        {
            std::lock_guard<std::mutex> l(m);  // (1)
            done = true;  // (2)
        }  // (3)
        cv.notify_one();  // (4)
    });  // (5)

    std::unique_lock<std::mutex> lock(m);  // (6)
    cv.wait(lock, [&done]() { return done; });  // (7)
}

void main() {
    foo();  // (8)
    t.join();  // (9)
}

新しく作成されたスレッドへのコンテキストスイッチがあると仮定します。スレッドtを作成した後、条件変数((5)と(6)の間のどこか)で待機を開始する前です。スレッドtはロック(1)を取得し、述語変数(2)を設定してから、ロック(3)を解放します。notify_one()(4)が実行される直前に、別のコンテキストスイッチがあると仮定します。メインスレッドはロック(6)を取得し、行(7)を実行します。その時点で述語が戻りtrue、待機する理由がないため、ロックを解放して続行します。foo(8)を返し、そのスコープ内の変数(を含むcv)は破棄されます。スレッドtがメインスレッド(9)に参加する前に、実行を終了する必要があるため、中断したところから実行を続行します。cv.notify_one()(4)、その時点cvですでに破壊されています!

この場合の可能な修正は、呼び出し時にロックを保持し続けることですnotify_one(つまり、行(3)で終わるスコープを削除します)。そうすることで、前のスレッドt呼び出しが新しく設定された述語変数をチェックして続行できるようにします 。チェックを行うには、現在保持しているロックを取得する必要があるためです。したがって、リターン後にスレッドがアクセスしないようにします。notify_onecv.waittcvtfoo

要約すると、この特定の場合の問題は、実際にはスレッド化ではなく、参照によってキャプチャされた変数の存続期間に関するものです。cvはスレッドを介した参照によってキャプチャされるtためcv、スレッドの実行中は存続していることを確認する必要があります。ここに示す他の例ではcondition_variablemutexオブジェクトがグローバルスコープで定義されているため、この問題は発生しません。したがって、プログラムが終了するまで、オブジェクトは存続することが保証されます。


1

@MichaelBurrは正しいです。condition_variable::notify_one変数をロックする必要はありません。例が示すように、そのような状況でロックを使用することを妨げるものは何もありません。

与えられた例では、ロックは変数の同時使用によって動機付けられていますisignalsスレッド変数を変更するため、その間、他のスレッドが変数にアクセスしないようにする必要があります。

ロックは同期が必要な状況で使用されますが、より一般的な方法で述べることはできないと思います。


もちろんですが、それに加えて、パターン全体が実際に機能するように、条件変数と組み合わせて使用​​する必要もあります。特に、条件変数wait関数は呼び出し内のロックを解放し、ロックを再取得した後にのみ戻ります。その後、「読書権」を取得したので、安全に状態を確認できます。それでもまだ待っているものではない場合は、に戻りますwait。これがパターンです。ところで、この例はそれを尊重していません。
v.oddou 2014年

1

場合によっては、cvが他のスレッドによって占有(ロック)されている可能性があります。notify _ *()の前に、ロックを取得して解放する必要があります。
そうでない場合は、notify _ *()がまったく実行されない可能性があります。


1

受け入れられた答えは誤解を招く可能性があると思うので、この答えを追加するだけです。すべてのケースでは、あなたが前notify_one()を呼び出すには、ミューテックスをロックする必要がありますどこかにあなたのコードは、スレッドセーフであるためにあなたが通知を呼び出す前に、実際にそれを再びアンロックかもしれませんが、_ *()。

明確にするために、wait()はlkのロックを解除し、ロックがロックされていない場合は未定義の動作になるため、wait(lk)に入る前にロックを取得する必要があります。これはnotify_one()の場合ではありませんが、あなたはあなたがコールすることはありませんようにする必要があり知らせる_ *()wait()を入力する前mutexのロック解除そのコールを持ちます。これは明らかに、notify _ *()を呼び出す前に同じミューテックスをロックすることによってのみ実行できます。

たとえば、次の場合を考えてみます。

std::atomic_int count;
std::mutex cancel_mutex;
std::condition_variable cancel_cv;

void stop()
{
  if (count.fetch_sub(1) == -999) // Reached -1000 ?
    cv.notify_one();
}

bool start()
{
  if (count.fetch_add(1) >= 0)
    return true;
  // Failure.
  stop();
  return false;
}

void cancel()
{
  if (count.fetch_sub(1000) == 0)  // Reached -1000?
    return;
  // Wait till count reached -1000.
  std::unique_lock<std::mutex> lk(cancel_mutex);
  cancel_cv.wait(lk);
}

警告:このコードにはバグが含まれています。

考え方は次のとおりです。スレッドはstart()とstop()をペアで呼び出しますが、start()がtrueを返した場合に限ります。例えば:

if (start())
{
  // Do stuff
  stop();
}

ある時点で1つの(他の)スレッドがcancel()を呼び出し、cancel()から戻った後、「Dostuff」で必要なオブジェクトを破棄します。ただし、start()とstop()の間にスレッドがある間、cancel()は戻らないはずであり、cancel()が最初の行を実行すると、start()は常にfalseを返すため、新しいスレッドは 'Doに入ることができません。スタッフのエリア。

正しく動作しますか?

理由は次のとおりです。

1)いずれかのスレッドがstart()の最初の行を正常に実行した場合(したがってtrueを返します)、cancel()の最初の行を実行したスレッドはまだありません(スレッドの総数は1000よりはるかに少ないと想定しています仕方)。

2)また、スレッドがstart()の最初の行を正常に実行したが、stop()の最初の行はまだ実行していない場合、どのスレッドもcancel()の最初の行を正常に実行することはできません(1つのスレッドのみに注意してください) cancel())を呼び出す:fetch_sub(1000)によって返される値は0より大きくなります。

3)スレッドがcancel()の最初の行を実行すると、start()の最初の行は常にfalseを返し、start()を呼び出すスレッドは「Dostuff」領域に入りません。

4)start()とstop()の呼び出し回数は常にバランスが取れているため、cancel()の最初の行の実行に失敗した後、stop()の(最後の)呼び出しによってカウントが発生する瞬間が常にあります。 -1000に到達するため、notify_one()が呼び出されます。キャンセルの最初の行でそのスレッドが失敗した場合にのみ発生する可能性があることに注意してください。

非常に多くのスレッドがstart()/ stop()を呼び出しているため、カウントが-1000に到達せず、cancel()が返されないという飢餓の問題とは別に、「ありそうもない、長くは続かない」と認められる可能性があります。別のバグがあります。

'Do stuff'領域内に1つのスレッドがある可能性があります。たとえば、stop()を呼び出しているだけです。その時点で、スレッドはcancel()の最初の行を実行し、fetch_sub(1000)を使用して値1を読み取り、フォールスルーします。ただし、ミューテックスを取得したり、wait(lk)を呼び出す前に、最初のスレッドはstop()の最初の行を実行し、-999を読み取り、cv.notify_one()を呼び出します。

次に、このnotify_one()の呼び出しは、条件変数でwait()を実行する前に実行されます。そして、プログラムは無期限にデッドロックします。

このため、wait()を呼び出すまでnotify_one()を呼び出すことはできません。条件変数の威力は、ミューテックスをアトミックにロック解除し、notify_one()の呼び出しが発生したかどうかを確認し、スリープ状態になるかどうかにあることに注意してください。あなたはそれをだますことはできませんが、やるあなたがfalseからtrueに条件を変更して、可能性がある変数に変更を加えるたびミューテックスをロックしておく必要性を保つため、ここで説明したようにあるため、競合状態のnotify_one()を呼び出している間、それはロックされています。

ただし、この例では条件はありません。条件として使用しなかったのはなぜですか 'count == -1000'?ここではまったく面白くないので、-1000に達するとすぐに、「Dostuff」領域に新しいスレッドが入ることはないと確信しています。さらに、スレッドは引き続きstart()を呼び出すことができ、カウントをインクリメントします(-999や-998など)が、それについては気にしません。重要なのは、-1000に到達したことだけです。これにより、「Dostuff」領域にスレッドがもうないことが確実にわかります。これはnotify_one()が呼び出されている場合に当てはまると確信していますが、cancel()がミューテックスをロックする前にnotify_one()を呼び出さないようにするにはどうすればよいですか?もちろん、notify_one()の直前にcancel_mutexをロックするだけでは役に立ちません。

問題は、条件を待っていないにもかかわらず、条件が残っいるため、ミューテックスをロックする必要があることです。

1)その条件に達する前2)notify_oneを呼び出す前。

したがって、正しいコードは次のようになります。

void stop()
{
  if (count.fetch_sub(1) == -999) // Reached -1000 ?
  {
    cancel_mutex.lock();
    cancel_mutex.unlock();
    cv.notify_one();
  }
}

[...同じstart()...]

void cancel()
{
  std::unique_lock<std::mutex> lk(cancel_mutex);
  if (count.fetch_sub(1000) == 0)
    return;
  cancel_cv.wait(lk);
}

もちろん、これはほんの一例ですが、他の場合も非常に似ています。条件変数を使用するほとんどすべての場合、notify_one()を呼び出す前にそのミューテックスを(すぐに)ロックする必要あります。そうでない場合は、wait()を呼び出す前に呼び出すことができます。

この場合、notify_one()を呼び出す前にミューテックスのロックを解除したことに注意してください。そうしないと、notify_one()を呼び出すと、条件変数を待機しているスレッドがウェイクアップし、ミューテックスを取得しようとする可能性があります。ミューテックスを再度解放する前に、ブロックします。それは必要以上に少し遅いです。

この例は、条件を変更する行がwait()を呼び出す同じスレッドによって実行されるという点でちょっと特別でした。

より一般的なのは、あるスレッドが条件が真になるのを単に待機し、別のスレッドがその条件に関係する変数を変更する前にロックを取得する場合です(条件が真になる可能性があります)。その場合、条件が真になる直前(および直後)にミューテックスロックされます。その場合、notify _ *()を呼び出す前にミューテックスのロックを解除するだけで問題ありません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.