Делает замок() гарантия приобретается в целях необходимости?
когда несколько потоков запрашивают блокировку одного и того же объекта, гарантирует ли среда 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 ответов:
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(); }
Это имеет несколько преимуществ:
порядок выполнения гарантированный.
вам больше не требуется явная блокировка (TPL заботится об этих деталях).
вам больше не нужны события и больше не нужно ждать всех событий. Вы можете просто сказать: дождитесь завершения последней задачи.
Если исключение было вызвано в любой из задач, последующие задачи будут прерваны, и исключение будет повторно создано вызовом Wait. Это может или не может соответствовать вашим желаниям поведение, но, как правило, лучшее поведение для последовательных, зависимых задач.
С помощью ТПЛ, вы добавили гибкости для будущего расширения, такие как поддержка отмены, ожидания на параллельные задачи для продолжения и др.
Я использую этот метод, чтобы сделать 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 будет заказывать выполнение действий, пока потоки ждут в блокировке.