pthreadの条件変数関数にミューテックスが必要なのはなぜですか?


182

私は読んでいpthread.hます。条件変数に関連する関数(などpthread_cond_wait(3))には、引数としてミューテックスが必要です。どうして?私が知る限りでは、その引数として使用するためだけにミューテックスを作成しますか?そのミューテックスは何をすることになっているのですか?

回答:


194

これは、条件変数が実装された(または元々は)実装された方法にすぎません。

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がまともな実装を持っていたからだろう:-)

いずれにしても、誤ったケースを処理した同じコードは、本物の偽のウェイクアップも処理しました。これは、作業可能フラグが設定されていないためです。


3
「何かを行う」は、whileループの中にあるべきではありません。あなたはあなたのwhileループが単に状態をチェックすることを望みます、さもなければ、あなたが偽の目覚めを得たならば、あなたは「何かをする」かもしれません。

1
いいえ、エラー処理はこれに次ぐものです。pthreadを使用すると、明らかな理由もなく(偽のウェイクアップ)、エラーなしで目を覚ますことができます。したがって、目が覚めた後、「何らかの状態」を再確認する必要があります。
nos

1
私は上手く理解できていない気がします。nosと同じ反応がありました。なぜループdo something内にあるのwhileですか?
ELLIOTTCABLE

1
多分私はそれを十分に明確にしていない。ループは、作業の準備ができるのを待たないため、実行できます。ループは、メインの「無限」ワークループです。cond_waitから戻り、作業フラグが設定されている場合は、作業を行ってから再びループします。「ある条件の間」は、スレッドがミューテックスを解放して終了する可能性が最も高い時点で作業を停止する場合にのみfalseになります。
paxdiablo

7
@stefaanv "mutexはまだ条件変数を保護するためのものであり、それを保護する方法は他にありません":mutexは条件変数を保護するためのものではありません。それは述語データを保護することですが、そのステートメントに続くコメントを読むことでそれを知っていると思います。条件変数を合法的に通知することができ、実装によって完全にサポートされ、述語をラップするミューテックスのロック解除、実際にそうすることで競合を緩和する場合があります。
WhozCraig 2013年

59

条件を通知することしかできない場合、条件変数は非常に制限されます。通常、通知された条件に関連するいくつかのデータを処理する必要があります。シグナリング/ウェイクアップは、競合状態を導入せずにそれを達成するためにアトミックに実行するか、過度に複雑にする必要があります

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ループにしないでください。
Maygarden裁判官、2010年

3
いいえ。本当に待っているのは、「some_data」がnull以外になることです。「初回」がnull以外の場合は、mutexを保持していて、データを安全に使用できます。do / whileループがあった場合、待機する前に誰かが条件変数にシグナルを送った場合、通知を見逃してしまいます(誰かがそれらを待つまでシグナルが送られるwin32のイベントのようなものではありません)
nos

4
私はこの質問につまずいたのですが、率直に言って、この答えは正しいですが、明確な欠陥のあるpaxdiabloの答えよりもはるかに少ないポイントしかありません(原子性はまだ必要ですが、mutexは条件の処理にのみ必要ですが、処理または通知用ではありません)。これがちょうどstackoverflowが機能する方法だと思います...
stefaanv

@stefaanv、欠陥を詳細に説明したい場合は、私の回答へのコメントとして、数か月後ではなくタイムリーにそれらを表示します:-)、私はそれらを修正させていただきます。あなたの短いフレーズは、あなたが言っていることを理解するのに十分な詳細を私に与えません。
paxdiablo 2013

1
@いいえ、すべきではありませwhile(some_data != NULL)while(some_data == NULL)か?
Eric Z

30

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

ワークキューを保護するにはミューテックスが必要です。条件変数自体は、仕事があるかどうかわからないことに注意してください。つまり、条件変数条件に関連付ける必要あり、その条件はコードで維持する必要があります。また、条件変数スレッド間で共有されるため、ミューテックスで保護する必要があります。


1
または、より簡潔に言うと、条件変数の要点は、アトミックな「ロック解除および待機」操作を提供することです。ミューテックスがなければ、ロックを解除するものは何もありません。
David Schwartz

ステートレスの意味を説明していただけませんか?
snr

@snr状態はありません。それらは、「ロック」または「シグナル」または「シグナルなし」ではありません。したがって、条件変数に関連付けられている状態を追跡するのはユーザーの責任です。たとえば、条件変数がスレッドにキューが空でなくなると通知する場合、1つのスレッドがキューを空にできず、他のスレッドがキューが空にならないことを知る必要がある場合に限られます。これは共有状態であり、ミューテックスで保護する必要があります。ウェイクアップメカニズムとして、mutexで保護された共有状態に関連して条件変数を使用できます。
David Schwartz、

16

すべての条件変数関数にミューテックスが必要なわけではありません。待機中の操作だけが必要です。シグナル操作とブロードキャスト操作には、ミューテックスは必要ありません。また、条件変数は特定のミューテックスに永続的に関連付けられていません。外部ミューテックスは条件変数を保護しません。条件変数に待機スレッドのキューなどの内部状態がある場合、これは条件変数内の内部ロックによって保護する必要があります。

次の理由により、待機操作は条件変数とミューテックスを結合します。

  • スレッドがmutexをロックし、共有変数に対して式を評価し、それがfalseであることが判明したため、待機する必要がある。
  • スレッドは、ミューテックスの所有からアトミックに待機するまで、アトミックに移動する必要があります。

このため、待機操作はミューテックスと条件の両方を引数として取り、スレッドがミューテックスの所有から待機へのアトミック転送を管理できるようにします。これにより、スレッドがウェイクアップレース条件の喪失の犠牲にならないようにします

スレッドがミューテックスを放棄し、ステートレスな同期オブジェクトで待機するが、アトミックではない方法で失われたウェイクアップレース状態が発生します。スレッドがロックを保持していない時間枠が存在し、オブジェクトの待機はまだ開始されていません。このウィンドウの間に、別のスレッドが入り、待機状態をtrueにし、ステートレス同期を通知してから消えることがあります。ステートレスオブジェクトは、信号が送られたことを覚えていません(ステートレスです)。そのため、元のスレッドはステートレス同期オブジェクトでスリープ状態になり、必要な条件がすでに真になっているにもかかわらず、ウェイクアップしません。ウェイクアップが失われました。

条件変数の待機関数は、ミューテックスを放棄する前に確実にウェイクアップをキャッチするように呼び出しスレッドが登録されていることを確認することにより、失われたウェイクアップを回避します。条件変数の待機関数がmutexを引数として使用しない場合、これは不可能です。


ブロードキャスト操作はミューテックスを取得する必要がないというリファレンスを提供できますか?MSVCでは、ブロードキャストは無視されます。
xvan 2017年

@xvan POSIX pthread_cond_broadcastpthread_cond_signal演算(このSOの質問について)では、mutexを引数として取りません。状態のみ。POSIX仕様はこちらです。ミューテックスは、ウェイクアップしたときに待機中のスレッドで何が起こるかについてのみ言及されています。
カズ

ステートレスの意味を説明していただけませんか?
snr

1
@snrステートレス同期オブジェクトは、シグナリングに関連する状態を記憶しません。通知されたときに、何かがその上で待機している場合は、それが起こされ、そうでない場合はウェイクアップが忘れられます。条件変数はこのようにステートレスです。同期を信頼できるものにするために必要な状態は、アプリケーションによって維持され、正しく記述されたロジックに従って、条件変数と組み合わせて使用​​されるミューテックスによって保護されます。
Kaz

7

他の回答がこのページほど簡潔で読みやすいとは思いません。通常、待機中のコードは次のようになります。

mutex.lock()
while(!check())
    condition.wait()
mutex.unlock()

wait()mutexでラップする理由は3つあります。

  1. ミューテックスがなければ、別のスレッドがそのsignal()前にできる可能性がありwait()、このウェイクアップを逃してしまいます。
  2. 通常check()は別のスレッドからの変更に依存しているため、とにかくスレッドを相互に除外する必要があります。
  3. 優先度の最も高いスレッドが最初に処理されるようにします(ミューテックスのキューにより、スケジューラは次に実行するユーザーを決定できます)。

3番目の点は必ずしも懸念事項ではありません。歴史的な背景が記事からこの会話にリンクされています。

このメカニズムに関して、偽のウェイクアップがよく言及されます(つまり、待機中のスレッドはsignal()呼び出されずに起こされます)。ただし、このようなイベントはループによって処理されcheck()ます。


4

条件変数は、回避するように設計された競合を回避できる唯一の方法であるため、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);

3

ミューテックスはあなたが呼び出すときにロックされることになっています pthread_cond_wait。これをアトミックに呼び出すと、ミューテックスのロックが解除され、条件でブロックされます。条件が通知されると、再びアトミックにロックして戻ります。

これにより、必要に応じて予測可能なスケジューリングを実装できるようになります。シグナリングを実行するスレッドは、ミューテックスが解放されて処理を実行するまで待機してから、条件をシグナリングできます。


では、ミューテックスを常にロック解除したままにし、待機する直前にロックし、待機が完了したらすぐにロックを解除しない理由はありますか?
ELLIOTTCABLE

ミューテックスはまた、待機スレッドとシグナルスレッド間の潜在的な競合を解決します。状態を変更して信号を送信するときにミューテックスが常にロックされている限り、信号を見逃して永遠に眠ることは決してないでしょう
Hasturkun

だから... ...私がすべき最初の待つオンミューテックスconditionvarのミューテックスに、conditionvarに待機する前に?全然わからない。
ELLIOTTCABLE

2
@elliottcable:mutexを保持せずに、待機する必要があるかどうかをどのようにして知ることができますか?以下のためにあなたは何を待っているものならばちょうど起こったのか?
David Schwartz

1

条件変数の実際の例が必要な場合は、クラスで練習問題を作成しました。

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

1

これは、概念的な必要性ではなく、特定の設計上の決定であると思われます。

pthreadのドキュメントによると、ミューテックスが分離されなかった理由は、ミューテックスを組み合わせることによって大幅なパフォーマンスの向上があり、ミューテックスを使用しない場合の一般的な競合状態のために、ほとんど常にとにかく行われることを期待しているためです。

https://linux.die.net/man/3/pthread_cond_wait

ミューテックスと条件変数の機能

ミューテックスの取得と解放を条件待機から切り離すことが提案されていました。これは、実際にはリアルタイムの実装を容易にするのは操作の組み合わせの性質であるため、拒否されました。これらの実装は、呼び出し元に対して透過的な方法で、条件変数とミューテックスの間で優先度の高いスレッドをアトミ​​ックに移動できます。これにより、余分なコンテキストの切り替えを防ぎ、待機中のスレッドにシグナルが送信されたときにミューテックスをより確定的に取得できます。したがって、公平性と優先順位の問題は、スケジューリングの規律によって直接処理できます。さらに、現在の条件待機操作は既存のプラクティスと一致しています。


0

それについてはたくさんの言い訳がありますが、私はそれを次の例で具体化したいと思います。

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てシグナルを送信しますが、待機中のスレッドがないため、スレッドは起こされません。親が再び走ると、それは永遠に眠ります、それは本当にひどいです。

ロックを個別に取得しているときに実行するとどうなりますか?

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