排他的にロックされたオブジェクトを変更するときの奇妙な動作-Monitor.Enter(x)
-
06-07-2019 - |
質問
Monitor.Enter()によって排他的にロックされているオブジェクトの参照を変更した場合に何が起こったかを見たかった。予想どおり、SynchronizationLockExceptionがスローされました。しかし、例外がスローされる前にいくつかのスレッドがモニターを通過するのを見て驚いた。
以下のコードの動作は次のとおりです。
- 100個のスレッドを作成して開始
- ManualResetEventが設定されるまで、すべてのスレッドを待機させます。
- ManualResetEventを設定します-インディレースで緑の旗を振るようなものです。
- xに排他ロックを設定(Monitor.Enter(x))
- xの参照を変更します。
この時点で、何らかの例外がスローされると予想していましたが、Monitor.Exit(x)までは発生しません。本当に奇妙なのは、例外が発生する前に10〜20個のスレッドがロックを通過できるように見えることです。それはどうして起こりますか?あるべきではないようです。もちろん、排他的にロックされたオブジェクトの参照を変更することは、ノーノーです。私は実際のコードでそれを決してしませんでしたが、それでも私は他のスレッドがモニターを通過するのを見て驚きました。あなたの考え?
using System;
using System.Threading;
namespace ThreadingPlayground
{
class Program
{
private int _value;
object x = new object();
ManualResetEvent _event = new ManualResetEvent(false);
static void Main()
{
Program p = new Program();
p.StartThreads();
Console.ReadLine();
}
private void StartThreads()
{
for(int i = 0;i<100;i++)
{
Thread t = new Thread(IncrementValue);
t.Start();
}
_event.Set();
}
private void IncrementValue()
{
WaitHandle.WaitAll(new WaitHandle[] {_event});
Monitor.Enter(x);
// Change the reference of the exclusively locked object. This
// allows other threads to enter, should this be possible?
x = Thread.CurrentThread.ManagedThreadId.ToString();
Console.WriteLine(++_value);
// throws a SynchronizationLockException
// but not until 10 - 20 more lines are written
Monitor.Exit(x);
}
}
}
解決
見ているのは期待される動作です。 Monitor.Enter()
に参照を渡すために使用される実際の変数について特別なことはありません。参照を変更しても、変数が新しい値を持ち、その参照はどこにもロックされないため、他のスレッドが排他ロックを取得することを妨げるべきではありません。
Exit
を呼び出すスレッドには、渡される参照に対する排他ロックがないため、問題は Exit
に付属しています。別のスレッドがロックオンしている可能性がありますしかし、実行中のスレッドはそうではありません。
これは、ご存じのとおり、参照が決して変更されない変数を使用してロックすることが常に最善である理由です。リソースの変数が変更される可能性がある場合は、新しい参照を使用します。
これで十分ですか?
他のヒント
'x'はオブジェクトへの参照です。オブジェクトそのものではありません。するとき
x = Thread.CurrentThread.ManagedThreadId.ToString();
xが以前参照していたロックされたオブジェクトを破棄し、xが他のオブジェクトを参照するようにしました。今すぐに
Monitor.Exit(x);
このオブジェクトは実際にはロックされていないため、例外が発生します。ロックしたオブジェクトは、ガベージコレクタによって収集されるガベージになりました。
この動作の理由は、ここでxの値を変更していることです:
x = Thread.CurrentThread.ManagedThreadId.ToString();
だからあなたが到着したとき
Monitor.Exit(x)
別のオブジェクトでロックを解除しています。あるキーで南京錠を入れて、別の南京錠からそのキーで南京錠を外そうとするようなものです。
さらに、Console.Writelineは他と比較して高価な命令であるため、複数のスレッドが1つがフィニッシュラインに近づく前にモニターに入ることができます。
実行例を次に示します。
多数のスレッドを開始します。
- スレッド
T1
は、オブジェクトx
でロックを取得します。 -
T2
は、オブジェクトx
でロックを取得しようとします。このスレッドは、永遠に待機することがわかっているため、運が悪いです。 -
T1
はx
を変更します。これは新しいオブジェクトです。x'1
と呼びます。 -
T3
はロックの取得を試みます。しかし、変数x
は実際にはオブジェクトx'1
を参照します。誰もx'1
をロックしていないため、彼は合格します。 -
T3
はx
を変更します。x'3
という新しいオブジェクトになりました。 -
T1
はコンソールに書き込みます。 -
T3
はコンソールに書き込みます。 -
T1
は、オブジェクトを指す変数x
でロックを解除しようとします...x'3
。 >
-
Monitor
オブジェクトには次のように表示されます:&quot;ねえ、あなたは何をしていると思う?あなたはそのロックを持っていません!この Exception 小さな吸盤を食べます - フィン