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