再帰的ロック(Mutex)と非再帰的ロック(Mutex)


183

POSIXでは、ミューテックスを再帰的にすることができます。つまり、同じスレッドが同じミューテックスを2回ロックしても、デッドロックは発生しません。もちろん、2回アンロックする必要もあります。そうしないと、他のスレッドがミューテックスを取得できません。pthreadをサポートするすべてのシステムが再帰的ミューテックスもサポートするわけではありませんが、POSIX準拠にしたい場合は、そうする必要があります。

他のAPI(より高レベルのAPI)も、通常はロックと呼ばれるミューテックスを提供します。一部のシステム/言語(Cocoa Objective-Cなど)では、再帰的mutexと非再帰的mutexの両方を提供しています。一部の言語では、どちらか一方しか提供されません。たとえば、Javaミューテックスは常に再帰的です(同じオブジェクトで同じスレッドが2回「同期」する場合があります)。それらが提供する他のスレッド機能によっては、再帰的ミューテックスは自分で簡単に記述できるため、再帰ミューテックスがなくても問題ない場合があります(私は、より単純なミューテックス/条件操作に基づいて、自分で再帰的ミューテックスをすでに実装しています)。

私が本当に理解していないこと:非再帰的mutexは何に適していますか?同じミューテックスを2回ロックする場合、なぜスレッドデッドロックが必要なのですか?それを回避できる高水準言語(たとえば、デッドロックが発生するかどうかをテストし、発生する場合は例外をスローする)でも、通常はそうしません。代わりにスレッドをデッドロックさせます。

これは、誤って2回ロックして1回だけロック解除した場合にのみ発生します。再帰的なmutexの場合、問題を見つけるのは難しくなります。そのため、すぐにデッドロックして、間違ったロックがどこにあるかを確認できますか?しかし、ロックを解除するときにロックカウンターを返すのと同じことはできませんでした。最後のロックを解放し、カウンターがゼロではない場合、例外をスローしたり、問題をログに記録したりできます。または、私が確認できない他のより有用な非再帰的ミューテックスのユースケースはありますか?それとも、非再帰的なミューテックスは再帰的なミューテックスよりもわずかに高速になる可能性があるため、それは単にパフォーマンスかもしれませんか?しかし、私はこれをテストしましたが、違いは実際にはそれほど大きくありません。

回答:


154

再帰的mutexと非再帰的mutexの違いは、所有権に関係しています。再帰的ミューテックスの場合、カーネルは、最初に最初にミューテックスを実際に取得したスレッドを追跡して、再帰と、代わりにブロックする必要がある別のスレッドとの違いを検出できるようにする必要があります。別の答えが指摘したように、このコンテキストを保存するためのメモリと、それを維持するために必要なサイクルの両方の点で、これの追加のオーバーヘッドの問題があります。

ただし、ここでも他の考慮事項があります。

再帰的mutexは所有権を持っているため、mutexを取得するスレッドは、mutexを解放するスレッドと同じでなければなりません。非再帰的mutexの場合、所有権はありません。どのスレッドも、最初にmutexを取得したスレッドに関係なく、通常mutexを解放できます。多くの場合、このタイプの「ミューテックス」は実際にはセマフォアクションに近く、必ずしもミューテックスを排他デバイスとして使用しているわけではなく、2つ以上のスレッド間の同期またはシグナリングデバイスとして使用しています。

ミューテックスの所有権に付属するもう1つのプロパティは、優先度の継承をサポートする機能です。カーネルはmutexを所有するスレッドとすべてのブロッカーのIDを追跡できるため、優先スレッドシステムでは、現在mutexを所有しているスレッドの優先度を最高優先度のスレッドの優先度に上げることができます。それは現在ミューテックスでブロックされています。この継承により、このような場合に発生する可能性のある優先順位の逆転の問題が回避されます。(すべてのシステムがそのようなミューテックスの優先度の継承をサポートしているわけではありませんが、所有権の概念によって可能になるもう1つの機能です)。

クラシックVxWorks RTOSカーネルを参照する場合、これらは3つのメカニズムを定義します。

  • mutex-再帰、およびオプションで優先度継承をサポートします。このメカニズムは、一貫してデータの重要なセクションを保護するために使用されます。
  • バイナリセマフォ -再帰なし、継承なし、単純な除外、テイカーとギバーは同じスレッドである必要はなく、ブロードキャストリリースが利用可能です。このメカニズムは、クリティカルセクションを保護するために使用できますが、スレッド間のコヒーレントシグナリングや同期にも特に役立ちます。
  • セマフォのカウント -再帰や継承は行われず、目的の初期カウントからの一貫したリソースカウンタとして機能し、スレッドは、リソースに対する正味のカウントがゼロの場合にのみブロックします。

繰り返しますが、これはプラットフォームによって多少異なります。特に、これらはこれらのものと呼ばれますが、これは、概念と実際のさまざまなメカニズムを表すものでなければなりません。


9
非再帰的mutexについての説明は、セマフォのように聞こえました。mutex(再帰的または非再帰的)には、所有権の概念があります。
Jay D

@JayD人々がこれらのようなことについて議論するとき、それは非常に混乱します...それで、これらのものを定義するエンティティは誰ですか?
Pacerier

13
@Pacerier関連する標準。この回答は、たとえばposix(pthreads)では間違っています。ロックを解除したスレッド以外のスレッドで通常のmutexをロック解除すると、未定義の動作になりますが、エラーチェックまたは再帰的なmutexで同じことを行うと、予測可能なエラーコードが発生します。他のシステムや標準では、動作が大きく異なる場合があります。
nos

おそらくこれはナイーブですが、ミューテックスの中心的な考え方は、ロックしているスレッドがミューテックスのロックを解除し、他のスレッドが同じことをするという印象を受けました。computing.llnl.gov/tutorials/pthreads
user657862

2
@curiousguy-ブロードキャストリリースは、セマフォでブロックされたすべてのスレッドを明示的に与えずに解放し(空のまま)、通常のバイナリギブは待機キューの先頭のスレッドのみを解放します(ブロックされているスレッドがあると想定)。
トールジェフ

123

答えは効率ではありません。再入不可のミューテックスはより良いコードにつながります。

例:A :: foo()がロックを取得します。次に、B :: bar()を呼び出します。あなたがそれを書いたとき、これはうまくいきました。しかし、しばらくしてから誰かがB :: bar()を変更してA :: baz()を呼び出し、これもロックを取得します。

まあ、再帰的なミューテックスがない場合、このデッドロックが発生します。持っている場合は実行されますが、壊れる可能性があります。A :: foo()は、mutexも取得するため、baz()が実行できなかったという前提で、bar()を呼び出す前にオブジェクトを不整合な状態のままにした可能性があります。しかし、おそらく実行すべきではありません!A :: foo()を作成した人は、誰も同時にA :: baz()を呼び出せないと想定していました。それが、これらのメソッドの両方がロックを取得した理由です。

mutexを使用するための正しいメンタルモデル:mutexは不変条件を保護します。ミューテックスが保持されている場合、インバリアントは変更される可能性がありますが、ミューテックスを解放する前に、インバリアントが再確立されます。再入可能ロックは、2回目にロックを取得したときに、不変条件がtrueであると確信できないため、危険です。

再入可能なロックに満足しているのは、このような問題を以前にデバッグする必要がなかったからです。ちなみに、Javaは最近、java.util.concurrent.locksに再入不可ロックを持っています。


4
もう一度ロックを取得したときに、不変条件が無効であることについてあなたが言っていることを理解するのにしばらく時間がかかりました。いい視点ね!それが読み取り/書き込みロック(JavaのReadWriteLockなど)で、読み取りロックを取得してから、同じスレッドでもう一度読み取りロックを再取得した場合はどうなるでしょうか。読み取りロックを取得した後、不変条件を無効にしないのですか?したがって、2番目の読み取りロックを取得しても、不変条件は依然として真です。
dgrant

1
@Jonathan最近、Javaのjava.util.concurrent.locksに再入不可ロックがありますか?
user454322 2012年

1
+1おそらく、リエントラントロックの最も一般的な使用法は、いくつかのメソッドが保護されたコードと保護されていないコードの両方から呼び出せる単一のクラスの内部にあると思います。これは実際には常に因数分解できます。@ user454322もちろん、Semaphore
maaartinus 2014

1
私の誤解を許してください、しかし私はこれがどのようにミューテックスに関連しているかはわかりません。マルチスレッドとロックが関係しておらず、をA::foo()呼び出す前にオブジェクトを一貫性のない状態のままにした可能性がありますA::bar()。再帰的かどうかに関係なく、ミューテックスはこのケースと何の関係がありますか?
Siyuan Ren

1
@SiyuanRen:問題は、コードについてローカルに推論できることです。人々(少なくとも私)は、ロックされた領域を不変の保守として認識するように訓練されています。つまり、ロックを取得したときに、他のスレッドが状態を変更していないため、クリティカル領域の不変条件が保持されます。これは難しいルールではありません。不変条件を考慮せずにコーディングすることはできますが、コードの推論と保守が難しくなるだけです。ミューテックスなしのシングルスレッドモードでも同じことが起こりますが、保護された領域をローカルで推論するように訓練されていません。
デビッドロドリゲス-2015年

92

Dave Butenhof自身が書いたように

「再帰的ミューテックスのすべての大きな問題の最大の問題は、ロックスキームとスコープを完全に見失うことを奨励することです。これは致命的です。それは悪です。それは「スレッドイーター」です。ロックを保持する時間は絶対に最短です。期間。常に。ロックが保持されていることを知らないため、または呼び出し先がミューテックスを必要とするかどうかがわからないためにロックが保持された状態で何かを呼び出している場合、保持している時間が長すぎます。アプリケーションにショットガンを向け、トリガーを引きます。おそらく、同時実行性を得るためにスレッドを使用し始めましたが、同時実行性を防止しました。」


9
またButenhofの応答の最後の部分に注意してください ...you're not DONE until they're [recursive mutex] all gone.. Or sit back and let someone else do the design.
user454322

2
彼はまた、マルチスレッドコードで外部ライブラリを使用し始めたときに、外部ライブラリの不変性を理解するという困難な作業を意識的に延期する松葉杖として、単一のグローバル再帰ミューテックス(彼の意見では1つだけ必要だと彼の意見は言う)を使用しても問題ないと述べています。しかし、松葉杖を永遠に使用するべきではありませんが、最終的にはコードの同時実行不変式を理解して修正するために時間を費やしてください。したがって、再帰的mutexを使用することは技術的な負債であると言い換えることができます。
FooF

13

mutexを使用するための正しいメンタルモデル:mutexは不変条件を保護します。

なぜこれがミューテックスを使用するための本当に正しいメンタルモデルであると確信していますか?適切なモデルはデータを保護しますが、不変式は保護しないと思います。

不変条件を保護する問題は、シングルスレッドアプリケーションにも存在し、マルチスレッドやミューテックスと共通するものはありません。

さらに、不変条件を保護する必要がある場合でも、再帰的でないバイナリセマフォを使用できます。


そうだね。不変条件を保護するためのより良いメカニズムがあります。
ActiveTrayPrntrTagDataStrDrvr 2014

8
これは、そのステートメントを提供した回答へのコメントでなければなりません。ミューテックスはデータを保護するだけでなく、不変条件も保護します。ミューテックスではなく、アトミック(データがそれ自体を保護する)の観点から単純なコンテナー(最も単純なのはスタック)を記述してみてください。そうすれば、ステートメントを理解できます。
デビッドロドリゲス-2015

mutexはデータを保護せず、不変条件を保護します。ただし、その不変式はデータの保護に使用できます。
Jon Hanna

4

再帰的mutexが役立つ主な理由の1つは、同じスレッドで複数回メソッドにアクセスする場合です。たとえば、ミューテックスロックが銀行A / cの引き出しを保護している場合、その引き出しにも手数料がかかる場合は、同じミューテックスを使用する必要があります。


4

再帰ミューテックスの唯一の良いユースケースは、オブジェクトに複数のメソッドが含まれている場合です。メソッドのいずれかがオブジェクトのコンテンツを変更するため、状態が再び整合する前にオブジェクトをロックする必要がある場合。

メソッドが他のメソッドを使用する場合(つまり、addNewArray()がaddNewPoint()を呼び出し、recheckBounds()で終了)、これらの関数自体がミューテックスをロックする必要がある場合、再帰的なミューテックスは有利です。

その他の場合(不適切なコーディングを解決し、別のオブジェクトでも使用する)は明らかに間違っています。


1

非再帰的mutexは何に適していますか?

何かをする前にミューテックスがロック解除されていることを確認する必要がある場合、それらは絶対に優れています。これは、pthread_mutex_unlockが再帰的でない場合にのみmutexがロック解除されることを保証できるからです。

pthread_mutex_t      g_mutex;

void foo()
{
    pthread_mutex_lock(&g_mutex);
    // Do something.
    pthread_mutex_unlock(&g_mutex);

    bar();
}

g_mutexが再帰的でない場合、上記のコードbar()はミューテックスがロックされていない状態で呼び出すことが保証されています。

したがってbar()、別のスレッドが同じミューテックスを取得しようとする可能性がある何かを実行する可能性のある未知の外部関数である場合に、デッドロックの可能性を排除します。このようなシナリオは、スレッドプール上に構築されたアプリケーションや分散アプリケーションで珍しくはありません。分散アプリケーションでは、プロセス間呼び出しが新しいスレッドを生成する可能性があり、クライアントプログラマはそれに気付くことさえありません。このようなすべてのシナリオでは、ロックが解放された後にのみ、上記の外部関数を呼び出すのが最善です。

g_mutex再帰的な場合、呼び出しを行う前にロックが解除されていることを確認する方法はありません

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