web-dev-qa-db-ja.com

lock()は要求された順序で取得されることを保証しますか?

複数のスレッドが同じオブジェクトのロックを要求する場合、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ロックを終了しています

61
Samuel Neff

IIRC、それはその順序である可能性が高いですが、保証はされていません。私は、少なくとも理論的にはスレッドが誤って起こされるケースがあると信じています。まだスレッドがロックされていないことに注意して、キューの後ろに移動してください。 Wait/Notifyだけの可能性はありますが、ロックのためにも潜入の疑いがあります。

Idefinitelyはそれに依存しません-シーケンスで発生することが必要な場合は、Queue<T>または同様のもの。

編集:私はこれをジョー・ダフィーの Windowsでの並行プログラミング 内に見つけました:これは基本的に同意します:

モニターはカーネルオブジェクトを内部で使用するため、OS同期メカニズムが示すのとほぼ同じFIFO動作を示します(前の章で説明)。モニターは不公平であるため、起動している待機中のスレッドがロックを取得しようとする前に別のスレッドがロックを取得しようとすると、不正なスレッドはロックを取得できます。

「おおよそのFIFO」ビットは、私が以前に考えていたものであり、「卑劣なスレッド」ビットは、FIFOの順序付けについて仮定をすべきではないことのさらなる証拠です。

73
Jon Skeet

lockステートメントは、その動作を実装するためにMonitorクラスを使用するように文書化されており、Monitorクラスのドキュメントでは、公平性については言及していません(私が見つけることができます)。したがって、要求されたロックが要求の順序で取得されることに依存するべきではありません。

実際、Jeffery Richterによる記事は、実際にlockは公平ではないと示しています。

確かに、これは古い記事なので変更されている可能性がありますが、Monitorクラスの公平性に関する契約では何も約束されていないため、最悪の事態を想定する必要があります。

10
Michael Burr

質問に少し正接しますが、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();
    }

これにはいくつかの利点があります。

  1. 実行順序は保証されています。

  2. 明示的なロックは不要になりました(TPLがこれらの詳細を処理します)。

  3. イベントは不要になり、すべてのイベントを待つ必要がなくなりました。簡単に言うと、最後のタスクが完了するのを待ちます。

  4. いずれかのタスクで例外がスローされた場合、後続のタスクは中止され、Waitの呼び出しによって例外が再スローされます。これは、希望する動作と一致する場合と一致しない場合がありますが、通常は、順次依存するタスクに最適な動作です。

  5. TPLを使用することで、キャンセルサポート、継続のための並列タスクの待機など、将来の拡張のための柔軟性が追加されました。

2
Dan Bryant

私はこの方法を使用して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は、スレッドがロックで待機している間にアクションの実行を順序付けます。

0
Noam