web-dev-qa-db-ja.com

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

_std::condition_variable_の使用について少し混乱しています。 condition_variable.wait()を呼び出す前に、mutexに_unique_lock_を作成する必要があることを理解しています。 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()に対してロックが取得されます。例のある他のページを見てみると、ほとんどの場合ロックを取得していないさまざまなものが見えます。

  • notify_one()を呼び出す前にミューテックスをロックすることを選択できますか?なぜロックすることを選択するのですか?
  • 上記の例では、最初のnotify_one()にはロックがないのに、後続の呼び出しにはロックがあります。この例は間違っていますか、それとも何らかの根拠がありますか?
70
Peter Smit

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

ただし、待機スレッドが実行可能になった場合(存在する場合)は、通知スレッドが保持しているロックをすぐに取得しようとするため、「悲観化」になる可能性があります。 notify_one()またはnotify_all()を呼び出している間、条件変数に関連付けられたロックを保持するのを避けるのが良い経験則だと思います。 notify_one()に相当するpthreadを呼び出す前にロックを解除する例については、 Pthread Mutex:pthread_mutex_unlock()が多くの時間を消費します を参照してください。

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


2016-02-27:競合状態がロックであるかどうかに関するコメントのいくつかの質問に対処するための大規模な更新は、notify_one()呼び出し。ほぼ2年前に質問が行われたため、この更新が遅れることはわかっていますが、プロデューサー(この例では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);
_

コンシューマがiをチェックするときに発生する可能性のある2つの状況があります。

  • iが0の場合、コンシューマはcv.wait()を呼び出します。実装のwait(lock)部分が呼び出されたとき、iは引き続き0です。ロックの使用はそれを保証します。この場合、コンシューマーがcondition_variable::notify_one()(およびcv.wait(lk, []{return i == 1;})を呼び出すまで、プロデューサーはwhileループでwait()を呼び出す機会がありません。呼び出しは、通知を適切に「キャッチ」するために必要なすべてを実行しました-wait()は、それを実行するまでロックを解除しません)。したがって、この場合、消費者は通知を見逃すことはできません。

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

ここの例には、done変数を使用して、コンシューマーが_i == 1_を認識したことをプロデューサースレッドに戻すという複雑さがありますが、これは分析をまったく変更しないと思います。 doneと_condition_variable_を含む同じクリティカルセクションにいる間に、i(読み取りと変更の両方)へのすべてのアクセスが行われます。

@ eh9が指し示した質問を見ると、 同期はstd :: atomicとstd :: condition_variableを使用して信頼できません の場合、will競合状態が表示されます。ただし、その質問に投稿されたコードは、条件変数の使用に関する基本的なルールの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)
}
_

_f->resume_mutex_を押しながら#3のwait()が実行されることに気付くでしょう。しかし、ステップ#1でwait()が必要かどうかのチェックはnotそのロックを保持しながら行われます(チェックアンドウェイトの場合はずっと少ない)。条件変数を適切に使用するための要件です)。そのコードスニペットに問題がある人は、_f->counter_は_std::atomic_型であるため、これが要件を満たすと考えていたと思います。ただし、_std::atomic_によって提供される原子性は、その後のf->resume.wait(lock)の呼び出しには拡張されません。この例では、_f->counter_がチェックされるとき(ステップ#1)とwait()が呼び出されるとき(ステップ#3)の間に競合があります。

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

63
Michael Burr

状況

Vc10とBoost 1.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 documentation の例によって支援されます。

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

問題

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

  • notify_one()not呼び出されていますが、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()を呼び出す前にmutexのロックを解除してはなりませんが、どちらの方法も正しいように見えます。

8

受け入れられた答えは誤解を招くかもしれないと思うので、この答えを追加するだけです。すべての場合において、コードをスレッドセーフにするためにnotify_one()somewhereを呼び出す前にミューテックスをロックする必要がありますが、ロックを解除することもできます再度notify _ *()を実際に呼び出す前に。

明確にするために、wait()はlkのロックを解除するため、wait(lk)を入力する前にロックを取得する必要があります。ロックがロックされていない場合は、未定義の動作になります。これはnotify_one()の場合ではありませんが、wait()を入力する前にnotify _ *()を呼び出さないことを確認する必要がありますandその呼び出しでミューテックスのロックを解除します。これは明らかに、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()から戻った後に 'Do stuff'で必要なオブジェクトを破棄します。ただし、cancel()はstart()とstop()の間にスレッドがある間は返されないことになっており、cancel()が最初の行を実行すると、start()は常にfalseを返すため、新しいスレッドは 'Doスタッフエリア。

うまくいく?

その理由は次のとおりです。

1)スレッドがstart()の最初の行を正常に実行する(したがってtrueを返す)場合、スレッドはまだcancel()の最初の行を実行していません(スレッドの合計数が方法)。

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

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

4)start()とstop()の呼び出し数は常にバランスが取れているため、cancel()の最初の行が正常に実行されなかった後、stop()の(最後の)呼び出しがcountを引き起こす瞬間が常にあります。 -1000に達するため、notify_one()が呼び出されます。キャンセルの最初の行がそのスレッドを通過させた場合にのみ発生することに注意してください。

非常に多くのスレッドがstart()/ stop()を呼び出してカウントが-1000に達しず、cancel()が戻らないという飢vの問題とは別に、「考えられないほど長く続くことはありません」として受け入れる可能性があります:

'Do stuff'エリア内に1つのスレッドが存在する可能性があります。これは単にstop()を呼び出していると言えます。その瞬間、スレッドは、fetch_sub(1000)を使用して値1を読み取り、フォールスルーするcancel()の最初の行を実行します。しかし、mutexを取得する前および/またはwait(lk)を呼び出す前に、最初のスレッドはstop()の最初の行を実行し、-999を読み取り、cv.notify_one()を呼び出します!

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

このため、notify_one()を呼び出すことはできませんntil wait()を呼び出しました。条件変数の能力は、ミューテックスをアトミックにロック解除し、notify_one()の呼び出しが発生したかどうかをチェックしてスリープ状態にできるかどうかにあります。それをだますことはできませんが、条件をfalseからtrueに変更する可能性のある変数に変更を加えるときは常にdo mutexをロックしたままにする必要があり、keep it whileここで説明したような競合状態のため、notify_one()を呼び出します。

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

問題は、条件を待機していないにもかかわらず、まだis条件があり、ミューテックスをロックする必要があることです。

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);
}

もちろん、これはほんの一例ですが、他のケースも非常によく似ています。条件変数を使用するほとんどすべての場合、need notify_one()を呼び出す前にそのmutexを(間もなく)ロックするか、wait()を呼び出す前に呼び出すことができます。

この場合、notify_one()を呼び出す前にmutexのロックを解除したことに注意してください。そうしないと、notify_one()の呼び出しが条件変数を待機しているスレッドを起こして(mutexミューテックスを再びリリースする前にブロックします。それは必要以上に少し遅いです。

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

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

1
Carlo Wood

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

1
Fan Jing

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

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

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

1
didierc