Делает замок() гарантия приобретается в целях необходимости?


когда несколько потоков запрашивают блокировку одного и того же объекта, гарантирует ли среда 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);
    }
}

гравюры:

Go введено: 0

0 у замка

0 спит для 49979998 тиков

Go введено: 1

Go введено: 2

Go введено: 3

Go введено: 4

Go введено: 5

Go введено: 6

Go введено: 7

Go введено: 8

Go введено: 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 56

5 ответов:

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

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

EDIT: я только что нашел это в Джо Даффи параллельное программирование на Windows который в основном согласен:

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

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

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

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

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

нормальные замки CLR не гарантированы, что будут FIFO.

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

слегка касательный к вопросу, но ThreadPool даже не гарантирует, что он будет выполнять поставленные в очередь рабочие элементы в том порядке, в котором они добавляются. Если вам нужно последовательное выполнение асинхронных задач, одним из вариантов является использование задач TPL (также перенесенных в .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. С помощью ТПЛ, вы добавили гибкости для будущего расширения, такие как поддержка отмены, ожидания на параллельные задачи для продолжения и др.

Я использую этот метод, чтобы сделать FIFO lock

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