2010-09-26 10 views
6

he desarrollado una cola productor-consumidor genérico que late por Monitor de la siguiente manera:hace Monitor.Wait Necesita sincronización?

la puesta en cola:

public void EnqueueTask(T task) 
    { 
     _workerQueue.Enqueue(task); 
     Monitor.Pulse(_locker); 
    } 

la retirada de cola:

private T Dequeue() 
    { 
     T dequeueItem; 
     if (_workerQueue.Count > 0) 
     { 
       _workerQueue.TryDequeue(out dequeueItem); 
      if(dequeueItem!=null) 
       return dequeueItem; 
     } 
     while (_workerQueue.Count == 0) 
      { 
       Monitor.Wait(_locker); 
     } 
     _workerQueue.TryDequeue(out dequeueItem); 
     return dequeueItem; 
    } 

la sección de espera produce el siguiente SynchronizationLockException: "se ha llamado al método de sincronización de objetos desde un bloque de código no sincronizado" ¿necesito sincronizarlo? por qué ? ¿Es mejor usar ManualResetEvents o la versión Slim de .NET 4.0?

Respuesta

6

Sí, el subproceso actual necesita "ser propietario" del monitor para llamar a Wait o Pulse, como se documenta. (Por lo tanto, también deberá bloquear el Pulse). No conozco los detalles de por qué es obligatorio, pero es lo mismo en Java. Sin embargo, normalmente he descubierto que querría hacer eso de todos modos, para que el código de llamada esté limpio.

Tenga en cuenta que Wait libera el monitor en sí, luego espera el Pulse, luego vuelve a adquirir el monitor antes de volver.

En cuanto al uso o ManualResetEventAutoResetEvent lugar - se puede, pero personalmente prefiero utilizar los métodos Monitor a menos que necesite algunas de las otras características de las manijas de espera (como atómicamente a la espera de cualquier/todos los múltiples asas).

+0

¿Por qué lo prefieres? ¿Cómo sincronizarías el Monitor? ¿Solo un bloqueo en el objeto de armario utilizado para el Monitor? ¿el candado no agrega otro cambio de contexto que el ResetEvents no necesita? – user437631

+0

@ user437631: Sí, solo una declaración de 'cerradura' normal está bien. Eso puede o no requerir un cambio de contexto adicional, y no creo que tenga ninguna evidencia de que ResetEvents no lo requiera. De hecho, como son objetos internos de CLR en lugar de objetos Win32 de proceso cruzado, los monitores son más livianos: espere que ResetEvents. –

2

partir de la descripción MSDN de Monitor.Wait():

libera el bloqueo en un objeto y bloquea el hilo de corriente hasta que se vuelve a adquirir la cerradura.

La parte 'libera el candado' es el problema, el objeto no está bloqueado. Está tratando el objeto _locker como si fuera un WaitHandle. Hacer su propio diseño de bloqueo que es probablemente correcto es una forma de magia negra que es mejor dejarle a nuestro curandero, Jeffrey Richter y Joe Duffy. Pero voy a dar a éste un tiro:

public class BlockingQueue<T> { 
    private Queue<T> queue = new Queue<T>(); 

    public void Enqueue(T obj) { 
     lock (queue) { 
      queue.Enqueue(obj); 
      Monitor.Pulse(queue); 
     } 
    } 

    public T Dequeue() { 
     T obj; 
     lock (queue) { 
      while (queue.Count == 0) { 
       Monitor.Wait(queue); 
      } 
      obj = queue.Dequeue(); 
     } 
     return obj; 
    } 
} 

En la mayoría de cualquier escenario productor/consumidor práctica tendrá que estrangular el productor por lo que no puede llenar la cola sin límites. Consulte Duffy's BoundedBuffer design para ver un ejemplo. Si puede permitirse mudarse a .NET 4.0, entonces definitivamente quiere aprovechar su clase ConcurrentQueue, tiene mucha más magia negra con bajo nivel de bloqueo y espera de giro.

0

La manera apropiada para ver Monitor.Wait y Monitor.Pulse/PulseAll no es de proporcionar un medio de espera, sino más bien (por Wait) como un medio de dejar que el sistema sabe que el código está en un bucle de espera que no puede salir hasta que algo de interés cambie, y (para Pulse/PulseAll) como un medio para hacerle saber al sistema que el código acaba de cambiar algo que pueda causar que se satisfaga la condición de salida del bucle de espera de otro hilo. Uno debería ser capaz de reemplazar todas las ocurrencias de Wait con Sleep(0) y aún tener el código funcionando correctamente (incluso si es mucho menos eficiente, como resultado de pasar tiempo de CPU probando repetidamente las condiciones que no han cambiado).

Para que este mecanismo funcione, es necesario para evitar la posibilidad de la siguiente secuencia:

  • El código en el bucle de espera prueba la condición cuando no es satisfecho.

  • El código en otro hilo cambia la condición para que se cumpla.

  • El código en ese otro hilo activa el bloqueo (que todavía nadie está esperando).

  • El código en el bucle de espera realiza un Wait ya que su condición no se cumplió.

El método Wait requiere que el hilo se espera tenga un seguro, ya que es la única manera en que puede estar seguro de que la condición que está esperando a que no va a cambiar entre el momento en que está a prueba y el tiempo que el código realiza la Wait. El método Pulse requiere un bloqueo porque esa es la única forma en que puede estar seguro de que si otro hilo se ha "comprometido" a realizar un Wait, el Pulse no ocurrirá hasta que el otro hilo realmente lo haga. Tenga en cuenta que usar Wait dentro de un candado no garantiza que se esté utilizando correctamente, pero no hay manera de que el uso de Wait fuera de un candado sea posiblemente correcto. El diseño Wait/Pulse realmente funciona razonablemente bien si ambas partes cooperan. Las mayores debilidades del diseño, en mi humilde opinión, son (1) no hay ningún mecanismo para que un hilo espere hasta que cualquiera de una serie de objetos sea pulsado; (2) incluso si uno está "apagando" un objeto de modo que todos los bucles de espera futuros deben salir inmediatamente (probablemente comprobando un indicador de salida), la única forma de asegurarse de que cualquier Wait al que se haya comprometido un hilo obtenga un Pulse es adquirir el candado, posiblemente esperando indefinidamente para que esté disponible.

Cuestiones relacionadas