2009-10-13 10 views

Respuesta

48

versión corta:

lock(obj) {...} 

es corto la mano para Monitor.Enter/Monitor.Exit (con gastos de envío, etc. excepción). Si nadie más tiene el bloqueo, puede obtenerlo (y ejecutar su código); de lo contrario, su hilo quedará bloqueado hasta que se adquiera el bloqueo (mediante otro hilo que lo suelte).

punto muerto que suele ocurrir cuando cualquiera de R: dos hilos se bloquean las cosas en diferentes órdenes:

thread 1: lock(objA) { lock (objB) { ... } } 
thread 2: lock(objB) { lock (objA) { ... } } 

(aquí, si cada uno de ellos adquieren la primera cerradura, y no las puede vez conseguir el segundo, ya que ni el hilo puede salir para liberar su bloqueo)

Este escenario se puede minimizar siempre bloqueando en el mismo orden; y puede recuperar (hasta cierto punto) utilizando Monitor.TryEnter (en lugar de Monitor.Enter/lock) y especificando un tiempo de espera.

o B: se puede bloquear a sí mismo con cosas como WinForms cuando hilos de conmutación, mientras que mantiene un bloqueo:

lock(obj) { // on worker 
    this.Invoke((MethodInvoker) delegate { // switch to UI 
     lock(obj) { // oopsiee! 
      ... 
     } 
    }); 
} 

El punto muerto parece obvia anteriormente, pero no es tan obvio cuando se tiene código espagueti; respuestas posibles: no cambie de hilo mientras mantiene bloqueos, o use BeginInvoke para que al menos pueda salir del bloqueo (permitiendo que se reproduzca la IU).


Wait/Pulse/PulseAll son diferentes; son para señalización.Yo uso este in this answer para señalar de manera que:

  • Dequeue: si intenta quitar de la cola de datos cuando la cola está vacía, se espera a que otro hilo para agregar datos, lo que despierta el hilo bloqueado
  • Enqueue: si tratas de datos en cola cuando la cola está llena, se espera a que otro hilo para eliminar los datos, lo que despierta el hilo bloqueado

Pulse sólo se despierta uno hilo - pero no soy lo suficientemente inteligente para probar que el próximo hilo i s siempre es el que yo quiero, entonces tiendo a usar PulseAll, y simplemente vuelvo a verificar las condiciones antes de continuar; como ejemplo:

 while (queue.Count >= maxSize) 
     { 
      Monitor.Wait(queue); 
     } 

Con este enfoque, puedo agregar con seguridad otros significados de Pulse, sin mi código existente asumiendo que "me desperté, por lo tanto, no hay datos" - lo cual es útil cuando (en el mismo ejemplo) Más tarde tuve que agregar un método Close().

1

Lea Jon Skeet's multi-part threading article.

Es realmente bueno. Las que menciona son aproximadamente un tercio del camino.

+7

ni siquiera menciona Pulso o ¡Espera! Vincular a un artículo de John Skeet no hace que una respuesta sea buena de manera automática ... –

+2

¿De verdad? ¿Qué es esto entonces? 'http: // www.yoda.arachsys.com/csharp/threads/deadlocks.shtml' – Geo

+0

@Geo: sí, este encaja mejor;) –

7

No, no lo protegen de interbloqueos. Son herramientas más flexibles para la sincronización de hilos. Aquí hay una muy buena explicación sobre cómo usarlos y un patrón de uso muy importante: sin este patrón, romperá todas las cosas: http://www.albahari.com/threading/part4.aspx

+1

PARA NOVICES ¡RECOMENDAMOS ALTAMENTE que la página de albahari sea perfecta! Pasa por el proceso de subprocesamiento y la sincronización paso a paso con ejemplos claros. – DanG

1

Son herramientas para sincronizar y señalizar entre hilos. Como tales, no hacen nada para evitar interbloqueos, pero si se utilizan correctamente, se pueden usar para sincronizar y comunicarse entre hilos.

Desafortunadamente, la mayor parte del trabajo necesario para escribir el código correcto multiproceso es actualmente responsabilidad de los desarrolladores en C# (y en muchos otros lenguajes). Eche un vistazo a cómo F #, Haskell y Clojure manejan esto para un enfoque completamente diferente.

37

Receta simple para usar Monitor.Wait y Monitor.Pulse. Se compone de un trabajador, un jefe y un teléfono que utilizan para comunicarse:

object phone = new object(); 

un "trabajador" hilo:

lock(phone) // Sort of "Turn the phone on while at work" 
{ 
    while(true) 
    { 
     Monitor.Wait(phone); // Wait for a signal from the boss 
     DoWork(); 
     Monitor.PulseAll(phone); // Signal boss we are done 
    } 
} 

un "jefe" hilo:

PrepareWork(); 
lock(phone) // Grab the phone when I have something ready for the worker 
{ 
    Monitor.PulseAll(phone); // Signal worker there is work to do 
    Monitor.Wait(phone); // Wait for the work to be done 
} 

Siguen ejemplos más complejos ...

A "Trabajador con otra cosa que hacer":

lock(phone) 
{ 
    while(true) 
    { 
     if(Monitor.Wait(phone,1000)) // Wait for one second at most 
     { 
      DoWork(); 
      Monitor.PulseAll(phone); // Signal boss we are done 
     } 
     else 
      DoSomethingElse(); 
    } 
} 

Un "Impaciente Boss":

PrepareWork(); 
lock(phone) 
{ 
    Monitor.PulseAll(phone); // Signal worker there is work to do 
    if(Monitor.Wait(phone,1000)) // Wait for one second at most 
     Console.Writeline("Good work!"); 
} 
+0

No lo entiendo ¿Cómo es posible que Boss y Worker estén bloqueados "por teléfono" al mismo tiempo? – Marshall

+1

@Marshall Monitor.Wait libera el bloqueo del "teléfono" al siguiente en la línea (presumiblemente al Jefe). –

+0

@DennisGorelik ahh, ya veo. He ampliado su punto [a continuación] (http://stackoverflow.com/a/42581381/1282864) – jdpilgrim

1

Desafortunadamente, ninguno de espera(), Pulso() o PulseAll() tiene la propiedad mágica que se desean para - y es que por utilizando este API evitará automáticamente el punto muerto.

considere el siguiente código

object incomingMessages = new object(); //signal object 

LoopOnMessages() 
{ 
    lock(incomingMessages) 
    { 
     Monitor.Wait(incomingMessages); 
    } 
    if (canGrabMessage()) handleMessage(); 
    // loop 
} 

ReceiveMessagesAndSignalWaiters() 
{ 
    awaitMessages(); 
    copyMessagesToReadyArea(); 
    lock(incomingMessages) { 
     Monitor.PulseAll(incomingMessages); //or Monitor.Pulse 
    } 
    awaitReadyAreaHasFreeSpace(); 
} 

Este código será un punto muerto! Quizás no hoy, quizás no mañana. Lo más probable es que cuando su código esté bajo estrés, de repente se haya vuelto popular o importante, y lo llamen para solucionar un problema urgente.

¿Por qué?

Finalmente, ocurrirá lo siguiente:

  1. Todas las roscas de los consumidores están haciendo un trabajo
  2. Los mensajes llegan, el área lista no puede contener más mensajes, y PulseAll() se llama.
  3. Ningún consumidor se despertó, porque ninguno está esperando
  4. Todas las roscas de consumo Llam.Esper() [DEADLOCK]

Este ejemplo particular asume que el hilo productor nunca se va a llamar PulseAll() de nuevo porque no tiene más espacio para colocar mensajes. Pero hay muchas, muchas variaciones rotas en este código. La gente va a tratar de hacerlo más robusto cambiando una línea como la fabricación de Monitor.Wait(); en

if (!canGrabMessage()) Monitor.Wait(incomingMessages); 

Por desgracia, que todavía no es suficiente para solucionarlo. Para solucionarlo se también necesidad de modificar el ámbito de bloqueo, donde Monitor.PulseAll() se llama:

LoopOnMessages() 
{ 
    lock(incomingMessages) 
    { 
     if (!canGrabMessage()) Monitor.Wait(incomingMessages); 
    } 
    if (canGrabMessage()) handleMessage(); 
    // loop 
} 

ReceiveMessagesAndSignalWaiters() 
{ 
    awaitMessagesArrive(); 
    lock(incomingMessages) 
    { 
     copyMessagesToReadyArea(); 
     Monitor.PulseAll(incomingMessages); //or Monitor.Pulse 
    } 
    awaitReadyAreaHasFreeSpace(); 
} 

El punto clave es que en el código fijo, las cerraduras restringen las posibles secuencias de eventos:

  1. A

    hilos de consumo hace su trabajo y bucles

  2. Ese hilo adquiere el bloqueo

    Y gracias al bloqueo ahora es cierto que :

  3. a. Mensajes tienen todavía no llegó a la zona preparada, y se libera el bloqueo llamando Espera() antes de la rosca receptor del mensaje puede adquirir el bloqueo y la copia Más mensajes en el área de lista, o

    b. Los mensajes tienen ya llegados en el área de listo y recibe los mensajes EN LUGAR DE llamar a Wait(). (Y si bien es tomar esta decisión es imposible que el hilo receptor del mensaje a, por ejemplo adquirir el bloqueo y la copia más mensajes a la zona preparada.)

Como resultado, el problema del código original ahora nunca ocurre : 3. Cuando PulseEvent() se llama Ningún consumidor se despertó, porque ninguno está esperando

Ahora observar que en este código que tiene que conseguir el alcance de cierre exactamente a la derecha. (Si, de hecho lo hizo bien!)

Y también, ya que se debe utilizar el lock (o Monitor.Enter() etc.) con el fin de utilizar Monitor.PulseAll() o Monitor.Wait() de una manera libre de punto muerto, todavía tiene que preocuparse por la posibilidad de otros bloqueos que suceden debido a ese bloqueo.

En pocas palabras: estas API también son fáciles de meter la pata y el punto muerto con, es decir, bastante peligroso

0

Algo que me totales arrojó aquí es que Pulse simplemente da un "mano a mano" con un hilo en un Wait .El subproceso Esperando no continuará hasta que el subproceso que hizo Pulseabandone el bloqueo y el subproceso de espera lo gane satisfactoriamente.

lock(phone) // Grab the phone 
{ 
    Monitor.PulseAll(phone); // Signal worker 
    Monitor.Wait(phone); // ****** The lock on phone has been given up! ****** 
} 

o

lock(phone) // Grab the phone when I have something ready for the worker 
{ 
    Monitor.PulseAll(phone); // Signal worker there is work to do 
    DoMoreWork(); 
} // ****** The lock on phone has been given up! ****** 

En ambos casos no es hasta que "la cerradura en el teléfono se ha renunciado a" otro hilo que puede conseguirlo.

Puede haber otros subprocesos esperando ese bloqueo de Monitor.Wait(phone) o lock(phone). Solo el que gane el candado podrá continuar.