Гарантируется ли гарантия 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 получил блокировку

1 спящий для 5001 тиков

1 выход из блокировки

2 получил блокировку

2 спальных места для 5001 тиков

2 выход из блокировки

3 получил блокировку

3 спальных для 5001 тиков

3 выход из блокировки

4 получил блокировку

4 спящего для 5001 тиков

4 выход из блокировки

5 получил блокировку

5 спальных мест для 5001 тиков

5 выход из замка

6 получил блокировку

6 выход из замка

7 получил блокировку

7 выход из блокировки

8 получил блокировку

8 выход из блокировки

9 получил блокировку

9 выход из блокировки

Ответ 1

IIRC, он, скорее всего, будет в таком порядке, но это не гарантируется. Я считаю, что есть, по крайней мере, теоретически случаи, когда поток будет просверлен ложно, обратите внимание, что он по-прежнему не имеет блокировки и идет в обратную сторону очереди. Возможно, что только для Wait/Notify, но у меня есть скрытое подозрение и для блокировки.

Я определенно не полагался на него - если вам нужно, чтобы в последовательности возникли ситуации, создайте Queue<T> или что-то подобное.

EDIT: Я только что нашел это в Joe Duffy Параллельное программирование в Windows, которое в основном соглашается:

Поскольку мониторы используют внутренние объекты ядра, они демонстрируют одно и то же поведение грубо-FIFO, которое также проявляет механизмы синхронизации ОС (описанные в предыдущей главе). Мониторы являются несправедливыми, поэтому, если другой поток пытается получить блокировку до того, как пробужденный ожидающий поток попытается получить блокировку, подлый поток разрешен для блокировки.

Бит "грубо-FIFO" - это то, о чем я думал раньше, а бит "скрытой нити" является еще одним доказательством того, что вы не должны делать предположений о заказе FIFO.

Ответ 2

Оператор lock документирован, чтобы использовать класс Monitor для реализации его поведения, а документы для класса Monitor не упоминают (что я могу найти) справедливости. Таким образом, вы не должны полагаться на запрашиваемые блокировки, полученные в порядке запроса.

Фактически, статья Джеффри Рихтера показывает, что lock не справедлив:

Предоставлено - это старая статья, поэтому все может измениться, но при условии, что в контракте для класса Monitor не существует promises класса справедливости, вам нужно принять худшее.

Ответ 3

Нормальные блокировки CLR не гарантируются как FIFO.

Но в этом ответе есть класс QueuedLock , который обеспечит гарантированное поведение блокировки FIFO.

Ответ 4

Немного касательно вопроса, но ThreadPool даже не гарантирует, что он выполнит заданные рабочие позиции в том порядке, в котором они будут добавлены. Если вам нужно выполнить последовательное выполнение асинхронных задач, один из них использует TPL Tasks (также поддерживаемый в .NET 3.5 через Reactive Extensions). Он будет выглядеть примерно так:

    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, вы добавили гибкость для будущего расширения, например, поддержку отмены, ожидание параллельных задач для продолжения и т.д.

Ответ 5

Я использую этот метод для блокировки 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 будет заказывать выполнение действий, пока потоки ждут в блокировке.