回答:
これは、条件変数が実装された(または元々は)実装された方法にすぎません。
mutexは、条件変数自体を保護するために使用されます。そのため、待機する前にロックする必要があります。
待機は「原子的に」ミューテックスをアンロックし、他の人が条件変数にアクセスできるようにします(シグナリング用)。次に、条件変数が通知またはブロードキャストされると、待機リストの1つ以上のスレッドが起こされ、ミューテックスがそのスレッドに対して再び魔法のようにロックされます。
通常、条件変数を使用した次の操作が表示され、それらがどのように機能するかを示します。次の例は、条件変数へのシグナルを介して作業が与えられるワーカースレッドです。
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.
待機が戻ったときに使用できるものがあれば、このループ内で作業が行われます。スレッドが作業を停止するようにフラグを立てられた場合(通常、別のスレッドが終了条件を設定し、条件変数をキックしてこのスレッドを起動します)、ループが終了し、ミューテックスがロック解除され、このスレッドが終了します。
上記のコードは、作業が行われている間mutexがロックされたままのシングルコンシューマーモデルです。マルチコンシューマーバリエーションの場合、例として使用できます。
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のペアでミューテックスで保護されていました。ただし、変更する前に他のスレッドがミューテックスをロックする必要がありました)。
それはでしたスレッドが別のプロセスによってキックされることなく条件待機から戻る技術的に可能でした(これは本物の偽のウェイクアップです)が、長年にわたってコードの開発/サービスとユーザーの両方でpthreadに取り組んでいましたそれらのうち、私は一度もこれらのいずれかを受け取ったことはありません。多分それはHPがまともな実装を持っていたからだろう:-)
いずれにしても、誤ったケースを処理した同じコードは、本物の偽のウェイクアップも処理しました。これは、作業可能フラグが設定されていないためです。
do something
内にあるのwhile
ですか?
条件を通知することしかできない場合、条件変数は非常に制限されます。通常、通知された条件に関連するいくつかのデータを処理する必要があります。シグナリング/ウェイクアップは、競合状態を導入せずにそれを達成するためにアトミックに実行するか、過度に複雑にする必要があります
pthreadは、技術的な理由から、疑似ウェイクアップも提供します。これは、述語をチェックする必要があることを意味します。これにより、条件が実際に通知されたことを確認でき、それを偽のウェイクアップと区別できます。待機することに関してそのような条件をチェックすることは保護される必要がある-したがって、条件変数は、その条件を保護するミューテックスをロック/ロック解除する間、原子的に待機/起動する方法を必要とする。
いくつかのデータが生成されたことが通知される単純な例を考えてみます。多分別のスレッドがあなたが望むいくつかのデータを作り、そのデータへのポインタを設定しました。
'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);
}
機能しません。起動してからmutexを取得するまでの間に競合状態が発生する可能性があります。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を上書きしないようにします)
while (some_data != NULL)
条件変数を少なくとも1回待機するように、do-whileループにしないでください。
while(some_data != NULL)
んwhile(some_data == NULL)
か?
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);
}
ワークキューを保護するにはミューテックスが必要です。条件変数自体は、仕事があるかどうかわからないことに注意してください。つまり、条件変数は条件に関連付ける必要があり、その条件はコードで維持する必要があります。また、条件変数はスレッド間で共有されるため、ミューテックスで保護する必要があります。
すべての条件変数関数にミューテックスが必要なわけではありません。待機中の操作だけが必要です。シグナル操作とブロードキャスト操作には、ミューテックスは必要ありません。また、条件変数は特定のミューテックスに永続的に関連付けられていません。外部ミューテックスは条件変数を保護しません。条件変数に待機スレッドのキューなどの内部状態がある場合、これは条件変数内の内部ロックによって保護する必要があります。
次の理由により、待機操作は条件変数とミューテックスを結合します。
このため、待機操作はミューテックスと条件の両方を引数として取り、スレッドがミューテックスの所有から待機へのアトミック転送を管理できるようにします。これにより、スレッドがウェイクアップレース条件の喪失の犠牲にならないようにします。
スレッドがミューテックスを放棄し、ステートレスな同期オブジェクトで待機するが、アトミックではない方法で失われたウェイクアップレース状態が発生します。スレッドがロックを保持していない時間枠が存在し、オブジェクトの待機はまだ開始されていません。このウィンドウの間に、別のスレッドが入り、待機状態をtrueにし、ステートレス同期を通知してから消えることがあります。ステートレスオブジェクトは、信号が送られたことを覚えていません(ステートレスです)。そのため、元のスレッドはステートレス同期オブジェクトでスリープ状態になり、必要な条件がすでに真になっているにもかかわらず、ウェイクアップしません。ウェイクアップが失われました。
条件変数の待機関数は、ミューテックスを放棄する前に確実にウェイクアップをキャッチするように呼び出しスレッドが登録されていることを確認することにより、失われたウェイクアップを回避します。条件変数の待機関数がmutexを引数として使用しない場合、これは不可能です。
他の回答がこのページほど簡潔で読みやすいとは思いません。通常、待機中のコードは次のようになります。
mutex.lock()
while(!check())
condition.wait()
mutex.unlock()
をwait()
mutexでラップする理由は3つあります。
signal()
前にできる可能性がありwait()
、このウェイクアップを逃してしまいます。check()
は別のスレッドからの変更に依存しているため、とにかくスレッドを相互に除外する必要があります。3番目の点は必ずしも懸念事項ではありません。歴史的な背景が記事からこの会話にリンクされています。
このメカニズムに関して、偽のウェイクアップがよく言及されます(つまり、待機中のスレッドはsignal()
呼び出されずに起こされます)。ただし、このようなイベントはループによって処理されcheck()
ます。
条件変数は、回避するように設計された競合を回避できる唯一の方法であるため、mutexに関連付けられています。
// 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は永久に待機します。
これを回避する唯一の方法は、条件変数が相互にアトミックにmutexを解放すると同時に、条件変数の待機を開始することです。これが、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のドキュメントによると、ミューテックスが分離されなかった理由は、ミューテックスを組み合わせることによって大幅なパフォーマンスの向上があり、ミューテックスを使用しない場合の一般的な競合状態のために、ほとんど常にとにかく行われることを期待しているためです。
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
スリープ状態に移行しようとします。しかし、sleep to go to sleepを呼び出す直前に、親は6〜7行の間で中断され、子が実行されます。子は状態変数done
をに変更
し1
てシグナルを送信しますが、待機中のスレッドがないため、スレッドは起こされません。親が再び走ると、それは永遠に眠ります、それは本当にひどいです。
ロックを個別に取得しているときに実行するとどうなりますか?