pthread.h
を読んでいます。条件変数関連の関数(pthread_cond_wait(3)
など)は、引数としてミューテックスを必要とします。どうして?私が知る限り、ミューテックスを作成しますjustその引数として使用しますか?そのミューテックスは何をすることになっていますか?
これは、条件変数が実装されている(または元々実装されていた)方法です。
ミューテックスは条件変数自体を保護するために使用されます。そのため、待機する前にロックする必要があります。
待機により、ミューテックスが「原子的に」ロック解除され、他のユーザーが条件変数にアクセスできるようになります(シグナリング用)。その後、条件変数が通知されるか、ブロードキャストされると、待機リストの1つ以上のスレッドが起動され、そのスレッドのmutexが再び魔法のようにロックされます。
通常、条件変数を使用した次の操作が表示され、その動作を示します。次の例は、条件変数へのシグナルを介して作業が与えられるワーカースレッドです。
thread:
initialise.
lock mutex.
while thread not told to stop working:
wait on condvar using mutex.
if work is available to be done:
do the work.
unlock mutex.
clean up.
exit thread.
待機が戻ったときに利用可能なものがある場合、このループ内で作業が行われます。作業の実行を停止するようにスレッドにフラグが付けられると(通常、別のスレッドが終了条件を設定してからこのスレッドを起動するために条件変数をキックすることにより)、ループが終了し、ミューテックスがロック解除され、このスレッドが終了します。
上記のコードは、作業の実行中はミューテックスがロックされたままになるため、単一消費者モデルです。複数の消費者のバリエーションでは、例として使用できます。
thread:
initialise.
lock mutex.
while thread not told to stop working:
wait on condvar using mutex.
if work is available to be done:
copy work to thread local storage.
unlock mutex.
do the work.
lock mutex.
unlock mutex.
clean up.
exit thread.
これにより、他の消費者が仕事をしている間に他の消費者が仕事を受け取ることができます。
条件変数は、何らかの条件をポーリングする負担から解放され、代わりに、何かが発生する必要があるときに別のスレッドから通知されるようにします。別のスレッドは、そのスレッドが次のように使用可能であることを通知できます。
lock mutex.
flag work as available.
signal condition variable.
unlock mutex.
多くの場合、誤ってスプリアスウェイクアップと呼ばれるものの大部分は、pthread_cond_wait
呼び出し(ブロードキャスト)内で複数のスレッドが通知され、1つはミューテックスで戻り、作業を行ってから待機するためです。
その後、実行する作業がないときに、2番目のシグナルスレッドが出てくる可能性があります。そのため、作業を行う必要があることを示す追加の変数が必要でした(これは本質的にここでcondvar/mutexペアでmutexで保護されていました-変更前にmutexをロックする必要がある他のスレッド)。
wasスレッドが別のプロセスによってキックされることなく条件待機から戻ることは技術的に可能です(これは真のスプリアスウェイクアップです)が、開発/サービスの両方で長年pthreadに取り組んできましたコードのユーザーとして、これらのいずれかを一度も受け取ったことはありません。多分それは、HPにまともな実装があったからだろう:-)
いずれにせよ、誤ったケースを処理した同じコードは、本物のスプリアスウェイクアップも処理しました。これは、ワークアベイラブルフラグが設定されていないためです。
条件のみを通知できる場合、条件変数はかなり制限されます。通常、通知された条件に関連するデータを処理する必要があります。シグナル/ウェイクアップは、競合状態を導入せずにそれを達成するために原子的に行われるか、過度に複雑でなければなりません
pthreadは、かなり技術的な理由から、 spurious wakeup を提供することもできます。つまり、述語をチェックする必要があるので、条件が実際に通知されたことを確認でき、それを偽のウェイクアップと区別できます。待機することに関してそのような状態をチェックすることはガードされる必要があります-したがって、条件変数は、その状態をガードしているミューテックスをロック/ロック解除する間にアトミックに待機/起動する方法を必要とします。
いくつかのデータが生成されたことが通知される単純な例を考えてみましょう。別のスレッドが必要なデータを作成し、そのデータへのポインターを設定した可能性があります。
プロデューサースレッドが「some_data」ポインターを介して別のコンシューマースレッドにデータを提供しているとします。
while(1) {
pthread_cond_wait(&cond); //imagine cond_wait did not have a mutex
char *data = some_data;
some_data = NULL;
handle(data);
}
自然に多くの競合状態が発生します。他のスレッドが目覚めた直後にsome_data = new_data
を実行した前にdata = some_data
を実行した場合
このケースを守るために独自のミューテックスを実際に作成することはできません。
while(1) {
pthread_cond_wait(&cond); //imagine cond_wait did not have a mutex
pthread_mutex_lock(&mutex);
char *data = some_data;
some_data = NULL;
pthread_mutex_unlock(&mutex);
handle(data);
}
動作しません。まだ起きてからミューテックスを取得するまでの間に競合状態になる可能性があります。 pthread_cond_waitの前にミューテックスを配置しても役に立ちません。待機中にミューテックスを保持するためです。つまり、プロデューサーはミューテックスを取得できません。 (この場合、2番目の条件変数を作成して、some_data
の処理が完了したことをプロデューサーに通知することができます。ただし、特に多くのプロデューサー/コンシューマーが必要な場合は、複雑になります。)
したがって、条件から待機/覚醒するときにミューテックスをアトミックに解放/取得する方法が必要です。それがpthread条件変数の機能であり、ここで行うことです。
while(1) {
pthread_mutex_lock(&mutex);
while(some_data == NULL) { // predicate to acccount for spurious wakeups,would also
// make it robust if there were several consumers
pthread_cond_wait(&cond,&mutex); //atomically lock/unlock mutex
}
char *data = some_data;
some_data = NULL;
pthread_mutex_unlock(&mutex);
handle(data);
}
(プロデューサーは当然同じ予防措置を講じ、常に同じmutexで 'some_data'を保護し、some_dataが現在!= NULLの場合にsome_dataを上書きしないようにする必要があります)
POSIX条件変数はステートレスです。したがって、状態を維持するのはあなたの責任です。状態は、待機するスレッドと他のスレッドに待機を停止するように指示するスレッドの両方によってアクセスされるため、mutexで保護する必要があります。ミューテックスなしで条件変数を使用できると思う場合、条件変数がステートレスであることを理解していません。
条件変数は、条件を中心に構築されます。条件変数で待機するスレッドは、何らかの条件を待機しています。条件変数を通知するスレッドは、その条件を変更します。たとえば、あるデータが到着するのをスレッドが待っている場合があります。他のスレッドは、データが到着したことに気付く場合があります。 「データが到着しました」が条件です。
単純化された条件変数の古典的な使用法を次に示します。
while(1)
{
pthread_mutex_lock(&work_mutex);
while (work_queue_empty()) // wait for work
pthread_cond_wait(&work_cv, &work_mutex);
work = get_work_from_queue(); // get work
pthread_mutex_unlock(&work_mutex);
do_work(work); // do that work
}
スレッドがどのように作業を待っているかを確認してください。作業はミューテックスによって保護されています。待機によりミューテックスが解放されるため、別のスレッドがこのスレッドに作業を与えることができます。通知方法は次のとおりです。
void AssignWork(WorkItem work)
{
pthread_mutex_lock(&work_mutex);
add_work_to_queue(work); // put work item on queue
pthread_cond_signal(&work_cv); // wake worker thread
pthread_mutex_unlock(&work_mutex);
}
作業キューを保護するためにミューテックスをneedすることに注意してください。条件変数自体には、作業があるかどうかがわからないことに注意してください。つまり、条件変数mustは条件に関連付けられ、その条件はコードで維持する必要があり、スレッド間で共有されるため、mutexで保護する必要があります。
すべての条件変数関数がミューテックスを必要とするわけではありません。待機操作のみが必要です。シグナルおよびブロードキャスト操作には、ミューテックスは必要ありません。また、条件変数は特定のミューテックスに永続的に関連付けられていません。外部ミューテックスは条件変数を保護しません。待機スレッドのキューなど、条件変数に内部状態がある場合、これは条件変数内の内部ロックによって保護する必要があります。
待機操作は、条件変数とミューテックスをまとめます。理由は次のとおりです。
このため、待機操作は引数としてミューテックスと条件の両方を取ります:ミューテックスの所有から待機へのスレッドのアトミック転送を管理できるように、スレッドがlost wakeの犠牲にならないようにします競合状態。
スレッドがミューテックスを放棄し、ステートレスな同期オブジェクトを待機するが、アトミックではない方法で失われたウェイクアップ競合状態が発生します:スレッドがロックを失い、オブジェクトの待機はまだ開始されていません。このウィンドウの間に、別のスレッドが入り、待機状態をtrueにし、ステートレス同期を通知してから消えます。ステートレスオブジェクトは、シグナルが送信されたことを記憶しません(ステートレスです)。そのため、元のスレッドは、ステートレス同期オブジェクトでスリープ状態になり、必要な条件が既に成立していても、ウェイクアップが失われても、ウェイクアップしません。
条件変数待機関数は、呼び出しスレッドがミューテックスを放棄する前に確実にウェイクアップをキャッチするように登録されていることを確認することにより、ウェイクアップの損失を回避します。条件変数の待機関数がミューテックスを引数として受け取らなかった場合、これは不可能です。
このページ ほど簡潔で読みやすい他の回答は見つかりません。通常、待機コードは次のようになります。
mutex.lock()
while(!check())
condition.wait()
mutex.unlock()
wait()
をミューテックスでラップする理由は3つあります。
signal()
の前にwait()
でき、このウェイクアップを見逃してしまいます。check()
は別のスレッドからの変更に依存しているため、とにかく相互排除する必要があります。3番目のポイントは常に懸念されるわけではありません-歴史的背景は記事から this conversation にリンクされています。
このメカニズムに関して、しばしばスプリアスウェイクアップが言及されています(つまり、signal()
が呼び出されずに待機スレッドが起動されます)。ただし、このようなイベントはループcheck()
によって処理されます。
条件変数は、それが回避するように設計されている競合を回避できる唯一の方法であるため、ミューテックスに関連付けられています。
// incorrect usage:
// thread 1:
while (notDone) {
pthread_mutex_lock(&mutex);
bool ready = protectedReadyToRunVariable
pthread_mutex_unlock(&mutex);
if (ready) {
doWork();
} else {
pthread_cond_wait(&cond1); // invalid syntax: this SHOULD have a mutex
}
}
// signalling thread
// thread 2:
prepareToRunThread1();
pthread_mutex_lock(&mutex);
protectedReadyToRuNVariable = true;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond1);
Now, lets look at a particularly nasty interleaving of these operations
pthread_mutex_lock(&mutex);
bool ready = protectedReadyToRunVariable;
pthread_mutex_unlock(&mutex);
pthread_mutex_lock(&mutex);
protectedReadyToRuNVariable = true;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond1);
if (ready) {
pthread_cond_wait(&cond1); // uh o!
この時点では、条件変数を通知するスレッドはないため、protectedReadyToRunVariableが準備ができていると言っても、thread1は永遠に待機します。
これを回避する唯一の方法は、条件変数をatomicallyにして、同時に条件変数の待機を開始しながらミューテックスを解放することです。これがcond_wait関数がミューテックスを必要とする理由です
// correct usage:
// thread 1:
while (notDone) {
pthread_mutex_lock(&mutex);
bool ready = protectedReadyToRunVariable
if (ready) {
pthread_mutex_unlock(&mutex);
doWork();
} else {
pthread_cond_wait(&mutex, &cond1);
}
}
// signalling thread
// thread 2:
prepareToRunThread1();
pthread_mutex_lock(&mutex);
protectedReadyToRuNVariable = true;
pthread_cond_signal(&mutex, &cond1);
pthread_mutex_unlock(&mutex);
ミューテックスは、pthread_cond_wait
を呼び出すとロックされることになっています。呼び出すと、アトミックにミューテックスのロックを解除し、条件でブロックします。条件が通知されると、再びアトミックにロックされて戻ります。
これにより、必要に応じて、予測可能なスケジューリングの実装が可能になります。つまり、シグナリングを実行するスレッドは、ミューテックスが解放されて処理を実行し、条件を通知するまで待機できます。
条件変数の実際の例が必要な場合は、クラスで演習を行いました。
#include "stdio.h"
#include "stdlib.h"
#include "pthread.h"
#include "unistd.h"
int compteur = 0;
pthread_cond_t varCond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex_compteur;
void attenteSeuil(arg)
{
pthread_mutex_lock(&mutex_compteur);
while(compteur < 10)
{
printf("Compteur : %d<10 so i am waiting...\n", compteur);
pthread_cond_wait(&varCond, &mutex_compteur);
}
printf("I waited nicely and now the compteur = %d\n", compteur);
pthread_mutex_unlock(&mutex_compteur);
pthread_exit(NULL);
}
void incrementCompteur(arg)
{
while(1)
{
pthread_mutex_lock(&mutex_compteur);
if(compteur == 10)
{
printf("Compteur = 10\n");
pthread_cond_signal(&varCond);
pthread_mutex_unlock(&mutex_compteur);
pthread_exit(NULL);
}
else
{
printf("Compteur ++\n");
compteur++;
}
pthread_mutex_unlock(&mutex_compteur);
}
}
int main(int argc, char const *argv[])
{
int i;
pthread_t threads[2];
pthread_mutex_init(&mutex_compteur, NULL);
pthread_create(&threads[0], NULL, incrementCompteur, NULL);
pthread_create(&threads[1], NULL, attenteSeuil, NULL);
pthread_exit(NULL);
}
これは、概念的な必要性ではなく、特定の設計上の決定のようです。
Pthreadのドキュメントによれば、mutexが分離されなかった理由は、mutexを組み合わせることでパフォーマンスが大幅に向上し、mutexを使用しない場合の一般的な競合状態のために、ほぼ常に行われることを期待しているからです。
https://linux.die.net/man/3/pthread_cond_wait
ミューテックスと条件変数の機能
ミューテックスの取得と解放を条件待機から切り離すことが提案されていました。これは、実際にはリアルタイム実装を容易にする操作の結合された性質であるため、拒否されました。これらの実装は、呼び出し元に対して透過的な方法で、条件変数とミューテックスの間で優先度の高いスレッドをアトミックに移動できます。これにより、余分なコンテキストの切り替えを防ぎ、待機中のスレッドに信号が送られたときにミューテックスをより確定的に取得できます。したがって、公平性と優先度の問題は、スケジューリング分野で直接対処できます。さらに、現在の状態の待機操作は、既存のプラクティスと一致します。
それについては多くの練習問題がありますが、次の例でそれを要約したいと思います。
1 void thr_child() {
2 done = 1;
3 pthread_cond_signal(&c);
4 }
5 void thr_parent() {
6 if (done == 0)
7 pthread_cond_wait(&c);
8 }
コードスニペットの何が問題になっていますか?先に進む前に、少し熟考してください。
問題は本当に微妙です。親がthr_parent()
を呼び出してからdone
の値を確認すると、それは0
であることがわかり、スリープ状態に移行しようとします。しかし、スリープ状態に入るためにwaitを呼び出す直前に、親は6〜7行の間で中断され、子が実行されます。子は状態変数done
を1
に変更してシグナルを送信しますが、待機しているスレッドはないため、スレッドは起動されません。親が再び走るとき、それは永遠に眠ります、それは本当にひどいです。
ロックを個別に取得しながら実行した場合はどうなりますか?