¿Se solicita la garantía lock () adquirida?

Cuando varios hilos solicitan un locking en el mismo objeto, ¿garantiza el CLR que los lockings se obtendrán en el orden en que se solicitaron?

Escribí una prueba para ver si esto era cierto, y parece indicar que sí, pero no estoy seguro de si esto es definitivo.

class LockSequence { private static readonly object _lock = new object(); private static DateTime _dueTime; public static void Test() { var states = new List(); _dueTime = DateTime.Now.AddSeconds(5); for (int i = 0; i  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); } } 

Huellas dactilares:

Ir ingresado: 0

0 obtuve locking

0 durmiendo por 49979998 garrapatas

Ir ingresado: 1

Ir ingresado: 2

Ir ingresado: 3

Ir ingresado: 4

Ir ingresado: 5

Ir ingresado: 6

Ir ingresado: 7

Ir ingresado: 8

Ir ingresado: 9

0 cerrando la cerradura

1 tiene cerradura

1 durmiendo por 5001 tics

1 cerradura de salida

2 obtuve locking

2 durmiendo por 5001 tics

2 cerrando la cerradura

3 obtuve locking

3 durmiendo por 5001 tics

3 salir del locking

4 obtuve locking

4 durmiendo por 5001 tics

4 salir del locking

5 obtuve locking

5 durmiendo por 5001 tics

5 salir del locking

6 tiene cerradura

6 salir del locking

7 obtuve locking

7 salir del locking

8 obtuve locking

8 salir del locking

9 consiguió locking

9 salir del locking

IIRC, es muy probable que sea en ese orden, pero no está garantizado. Creo que hay al menos casos teóricos en los que un hilo se despertará espurosamente, tenga en cuenta que todavía no tiene el locking y vaya al final de la cola. Es posible que sea solo para Wait / Notify , pero tengo la sospecha de que también es para bloquear.

Definitivamente no confiaría en eso; si necesitas que las cosas sucedan en una secuencia, crea una Queue o algo similar.

EDITAR: Acabo de encontrar esto dentro de la Progtwigción simultánea de Joe Duffy en Windows, que básicamente concuerda:

Debido a que los monitores usan objetos kernel internamente, exhiben el mismo comportamiento aproximado de FIFO que los mecanismos de sincronización del sistema operativo también exhiben (descrito en el capítulo anterior). Los monitores son injustos, por lo que si otro hilo intenta adquirir el locking antes de que un hilo en espera despierto intente adquirir el locking, el hilo engañoso puede adquirir un locking.

El bit “roughly-FIFO” es lo que estaba pensando antes, y el bit “sneaky thread” es una prueba más de que no debe hacer suposiciones sobre el orden FIFO.

La statement de lock está documentada para usar la clase Monitor para implementar su comportamiento, y los documentos para la clase Monitor no hacen mención (que puedo encontrar) de imparcialidad. Por lo tanto, no debe confiar en que los lockings solicitados se adquieran en el orden de solicitud.

De hecho, un artículo de Jeffery Richter indica que de hecho el lock no es justo:

  • Equidad de sincronización de subprocesos en .NET CLR

De acuerdo, es un artículo antiguo, por lo que las cosas pueden haber cambiado, pero dado que no se hacen promesas en el contrato para la clase Monitor sobre justicia, es necesario asumir lo peor.

No se garantiza que los lockings CLR normales sean FIFO.

Pero, hay una clase QueuedLock en esta respuesta que proporcionará un comportamiento de locking FIFO garantizado .

Ligeramente tangente a la pregunta, pero ThreadPool ni siquiera garantiza que ejecutará elementos de trabajo en cola en el orden en que se agregan. Si necesita la ejecución secuencial de tareas asíncronas, una opción es usar Tareas TPL (también backported a .NET 3.5 mediante Reactive Extensions ). Se vería algo como esto:

  public static void Test() { var states = new List(); _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(); } 

Esto tiene algunas ventajas:

  1. La orden de ejecución está garantizada.

  2. Ya no necesita un locking explícito (el TPL se ocupa de esos detalles).

  3. Ya no necesita eventos y ya no necesita esperar en todos los eventos. Simplemente puede decir: espere a que se complete la última tarea.

  4. Si se lanzara una excepción en cualquiera de las tareas, las tareas subsiguientes se anularían y la excepción se volvería a generar mediante la llamada a Esperar. Esto puede o no coincidir con su comportamiento deseado, pero generalmente es el mejor comportamiento para tareas dependientes secuenciales.

  5. Al usar el TPL, ha agregado flexibilidad para futuras expansiones, como soporte de cancelación, esperando tareas paralelas para continuar, etc.

Estoy usando este método para hacer el locking FIFO

 public class QueuedActions { private readonly object _internalSyncronizer = new object(); private readonly ConcurrentQueue _actionsQueue = new ConcurrentQueue(); 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 ordenará la ejecución de las acciones mientras los hilos están esperando en el locking.