複数のスレッドが同じオブジェクトのロックを要求する場合、CLRは要求された順序でロックが取得されることを保証しますか?
これが真実であるかどうかを確認するためのテストを書きましたが、それは真実を示しているようですが、これが決定的であるかどうかはわかりません。
class LockSequence
{
private static readonly object _lock = new object();
private static DateTime _dueTime;
public static void Test()
{
var states = new List<State>();
_dueTime = DateTime.Now.AddSeconds(5);
for (int i = 0; i < 10; i++)
{
var state = new State {Index = i};
ThreadPool.QueueUserWorkItem(Go, state);
states.Add(state);
Thread.Sleep(100);
}
states.ForEach(s => s.Sync.WaitOne());
states.ForEach(s => s.Sync.Close());
}
private static void Go(object state)
{
var s = (State) state;
Console.WriteLine("Go entered: " + s.Index);
lock (_lock)
{
Console.WriteLine("{0,2} got lock", s.Index);
if (_dueTime > DateTime.Now)
{
var time = _dueTime - DateTime.Now;
Console.WriteLine("{0,2} sleeping for {1} ticks", s.Index, time.Ticks);
Thread.Sleep(time);
}
Console.WriteLine("{0,2} exiting lock", s.Index);
}
s.Sync.Set();
}
private class State
{
public int Index;
public readonly ManualResetEvent Sync = new ManualResetEvent(false);
}
}
プリント:
入力済み:0
0はロックされました
0 49979998ティックの間スリープ
入力してください:1
入力してください:2
入力してください:3
入力してください:4
入力してください:5
入力してください:6
入力してください:7
入力してください:8
入力してください:9
0ロックを終了しています
1がロックされました
5001ティックの間1睡眠
1個のロックを終了しています
2がロックされました
5001ティックの間眠っている2
2ロックを終了しています
3ロックを取得しました
5001ティックで3睡眠
3ロックを終了しています
4はロックを取得しました
5001ティックで4睡眠
4ロックを終了しています
5がロックされました
5001ティックで5睡眠
5ロックを終了しています
6がロックされました
6ロックを終了しています
7がロックされました
7ロックを終了しています
8がロックされました
8ロックを終了しています
9がロックされました
9ロックを終了しています
IIRC、それはその順序である可能性が高いですが、保証はされていません。私は、少なくとも理論的にはスレッドが誤って起こされるケースがあると信じています。まだスレッドがロックされていないことに注意して、キューの後ろに移動してください。 Wait
/Notify
だけの可能性はありますが、ロックのためにも潜入の疑いがあります。
Idefinitelyはそれに依存しません-シーケンスで発生することが必要な場合は、Queue<T>
または同様のもの。
編集:私はこれをジョー・ダフィーの Windowsでの並行プログラミング 内に見つけました:これは基本的に同意します:
モニターはカーネルオブジェクトを内部で使用するため、OS同期メカニズムが示すのとほぼ同じFIFO動作を示します(前の章で説明)。モニターは不公平であるため、起動している待機中のスレッドがロックを取得しようとする前に別のスレッドがロックを取得しようとすると、不正なスレッドはロックを取得できます。
「おおよそのFIFO」ビットは、私が以前に考えていたものであり、「卑劣なスレッド」ビットは、FIFOの順序付けについて仮定をすべきではないことのさらなる証拠です。
lock
ステートメントは、その動作を実装するためにMonitor
クラスを使用するように文書化されており、Monitorクラスのドキュメントでは、公平性については言及していません(私が見つけることができます)。したがって、要求されたロックが要求の順序で取得されることに依存するべきではありません。
実際、Jeffery Richterによる記事は、実際にlock
は公平ではないと示しています。
確かに、これは古い記事なので変更されている可能性がありますが、Monitor
クラスの公平性に関する契約では何も約束されていないため、最悪の事態を想定する必要があります。
質問に少し正接しますが、ThreadPoolは、キューに入れられた作業項目が追加された順序で実行されることを保証しません。非同期タスクの順次実行が必要な場合、1つのオプションはTPLタスクを使用することです(これも Reactive Extensions を介して.NET 3.5にバックポートされます)。次のようになります。
public static void Test()
{
var states = new List<State>();
_dueTime = DateTime.Now.AddSeconds(5);
var initialState = new State() { Index = 0 };
var initialTask = new Task(Go, initialState);
Task priorTask = initialTask;
for (int i = 1; i < 10; i++)
{
var state = new State { Index = i };
priorTask = priorTask.ContinueWith(t => Go(state));
states.Add(state);
Thread.Sleep(100);
}
Task finalTask = priorTask;
initialTask.Start();
finalTask.Wait();
}
これにはいくつかの利点があります。
実行順序は保証されています。
明示的なロックは不要になりました(TPLがこれらの詳細を処理します)。
イベントは不要になり、すべてのイベントを待つ必要がなくなりました。簡単に言うと、最後のタスクが完了するのを待ちます。
いずれかのタスクで例外がスローされた場合、後続のタスクは中止され、Waitの呼び出しによって例外が再スローされます。これは、希望する動作と一致する場合と一致しない場合がありますが、通常は、順次依存するタスクに最適な動作です。
TPLを使用することで、キャンセルサポート、継続のための並列タスクの待機など、将来の拡張のための柔軟性が追加されました。
私はこの方法を使用してFIFOロックします
public class QueuedActions
{
private readonly object _internalSyncronizer = new object();
private readonly ConcurrentQueue<Action> _actionsQueue = new ConcurrentQueue<Action>();
public void Execute(Action action)
{
// ReSharper disable once InconsistentlySynchronizedField
_actionsQueue.Enqueue(action);
lock (_internalSyncronizer)
{
Action nextAction;
if (_actionsQueue.TryDequeue(out nextAction))
{
nextAction.Invoke();
}
else
{
throw new Exception("Something is wrong. How come there is nothing in the queue?");
}
}
}
}
ConcurrentQueueは、スレッドがロックで待機している間にアクションの実行を順序付けます。