私は読んでいpthread.h
ます。条件変数関連の関数 ( などpthread_cond_wait(3)
) には、引数としてミューテックスが必要です。なんで?私が知る限り、その引数として使用するためだけにミューテックスを作成するつもりですか? そのミューテックスは何をすることになっていますか?
10 に答える
これは、条件変数が実装されている (または元々実装されていた) 方法です。
ミューテックスは、条件変数自体を保護するために使用されます。そのため、待機する前にロックする必要があります。
待機はミューテックスを「アトミックに」ロック解除し、他のユーザーが条件変数にアクセスできるようにします(シグナリング用)。次に、条件変数が通知またはブロードキャストされると、待機リストの 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 ペアで本質的にミューテックス保護されていました - ただし、ミューテックスを変更する前に他のスレッドがミューテックスをロックする必要がありました)。
スレッドが別のプロセスによってキックされることなく状態待機から戻ることは技術的に可能でした (これは本物の偽のウェイクアップです)。それらのうち、私は一度もこれらのものを受け取ったことはありません。たぶんそれは、HPがまともな実装をしていたからです:-)
いずれにせよ、誤ったケースを処理したのと同じコードが本物の偽のウェイクアップも処理しました。
条件を通知することしかできない場合、条件変数は非常に制限されます。通常、通知された条件に関連するデータを処理する必要があります。シグナリング/ウェイクアップは、競合状態を導入せずにそれを達成するためにアトミックに実行する必要があるか、過度に複雑にする必要があります
pthreads は、かなり技術的な理由から、偽の 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
このケースを保護するために独自のミューテックスを実際に作成することはできません.eg
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);
}
(プロデューサーは当然、同じ予防措置を講じ、常に同じミューテックスで「some_data」を保護し、some_data が現在 != NULL の場合に some_data を上書きしないようにする必要があります)
POSIX 条件変数はステートレスです。したがって、状態を維持するのはあなたの責任です。状態は、待機しているスレッドと他のスレッドに待機を停止するように指示するスレッドの両方からアクセスされるため、ミューテックスによって保護する必要があります。ミューテックスなしで条件変数を使用できると思われる場合は、条件変数がステートレスであることを理解していません。
条件変数は、条件を中心に構築されます。条件変数を待機するスレッドは、何らかの条件を待機しています。条件変数を通知するスレッドは、その条件を変更します。たとえば、スレッドが何らかのデータの到着を待機している場合があります。他のスレッドが、データが到着したことに気付く場合があります。「データが到着しました」が条件です。
単純化された条件変数の古典的な使用法を次に示します。
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);
}
作業キューを保護するためにミューテックスが必要であることに注意してください。条件変数自体には、仕事があるかどうかがわからないことに注意してください。つまり、条件変数は条件に関連付ける必要があり、その条件はコードで維持する必要があり、スレッド間で共有されるため、ミューテックスで保護する必要があります。
このページほど簡潔で読みやすい他の回答は見つかりません。通常、待機中のコードは次のようになります。
mutex.lock()
while(!check())
condition.wait(mutex) # atomically unlocks mutex and sleeps. Calls
# mutex.lock() once the thread wakes up.
mutex.unlock()
wait()
をミューテックスでラップする理由は 3 つあります。
- ミューテックスがなければ、別のスレッドが
signal()
の前にある可能性がwait()
あり、このウェイクアップを見逃す可能性があります。 - 通常
check()
、別のスレッドからの変更に依存しているため、とにかく相互排除が必要です。 - 最も優先度の高いスレッドが最初に処理されるようにします (ミューテックスのキューにより、スケジューラは次に誰が処理されるかを決定できます)。
3 番目の点は、常に懸念事項であるとは限りません。記事からこの会話まで、歴史的な背景が関連付けられています。
このメカニズムに関しては、偽のウェイクアップがよく言及されます (つまり、待機中のスレッドがsignal()
呼び出されずにウェイクアップされます)。ただし、そのようなイベントは looped によって処理され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 は永遠に待機します。
これを回避する唯一の方法は、条件変数がアトミックにミューテックスを解放すると同時に、条件変数の待機を開始することです。これが、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
。アトミックに呼び出すと、ミューテックスのロックが解除され、条件でブロックされます。条件が通知されると、アトミックに再度ロックして戻ります。
これにより、必要に応じて予測可能なスケジューリングを実装できます。つまり、シグナル伝達を行うスレッドは、mutex が解放されて処理を実行し、条件を通知するまで待機できます。
条件変数の実際の例が必要な場合は、クラスで演習を行いました。
#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);
}