Получена ли гарантия lock () в запрашиваемом порядке? - PullRequest
59 голосов
/ 19 ноября 2010

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

Ответы [ 5 ]

73 голосов
/ 19 ноября 2010

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

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

РЕДАКТИРОВАТЬ: Я только что нашел это в параллельного программирования Джо Даффи в Windows , который в основном согласен:

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

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

10 голосов
/ 02 июня 2015

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

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

10 голосов
/ 19 ноября 2010

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

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

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

2 голосов
/ 19 ноября 2010

Немного касательно вопроса, но ThreadPool даже не гарантирует, что выполнит поставленные в очередь рабочие элементы в порядке их добавления. Если вам требуется последовательное выполнение асинхронных задач, одним из вариантов является использование задач TPL (также переносимых в .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();
    }

Это имеет несколько преимуществ:

  1. Выполнение заказа гарантировано.

  2. Вам больше не требуется явная блокировка (TPL заботится об этих деталях).

  3. Вам больше не нужны события и больше не нужно ждать всех событий. Вы можете просто сказать: дождитесь завершения последнего задания.

  4. Если в какой-либо из задач было сгенерировано исключение, последующие задачи будут прерваны, а исключение будет переброшено вызовом Wait. Это может соответствовать или не соответствовать вашему желаемому поведению, но обычно это лучшее поведение для последовательных зависимых задач.

  5. Используя TPL, вы добавили гибкость для будущего расширения, такого как поддержка отмены, ожидание продолжения параллельных задач и т. Д.

0 голосов
/ 26 марта 2016

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

...