2010-04-29 37 views
5

He tenido el siguiente código en mi aplicación durante algunos años y nunca he visto un problema.Colas de espera y colas en C#

while ((PendingOrders.Count > 0) || (WaitHandle.WaitAny(CommandEventArr) != 1)) 
{ 
    lock (PendingOrders) 
    { 
     if (PendingOrders.Count > 0) 
     { 
      fbo = PendingOrders.Dequeue(); 
     } 
     else 
     { 
      fbo = null; 
     } 
    } 

    // Do Some Work if fbo is != null 
} 

Dónde CommandEventArr se compone de la NewOrderEvent (un evento de reinicio automático) y el ExitEvent (un evento de rearme manual).

Pero no estoy seguro de si esto es seguro para subprocesos (suponiendo N hilos de productor que bloquean la cola antes de enqueing y un hilo de consumidor que ejecuta el código anterior). Además, podemos suponer que la propiedad Queue.Count simplemente devuelve un valor de Int32 de instancia de la clase Queue (sin volátil o interbloqueado o un bloqueo, etc.).

¿Cuál es el patrón habitual utilizado con una Cola y un AutoResetEvent para solucionar esto y hacer lo que estoy tratando de hacer con el código anterior?

(Modificado para cambiar la pregunta un poco después de haber sido señalado correctamente que Queue.Count podría hacer algo y su implementación específica).

+0

Seguramente este lazo está incrustado en algún tipo de bucle externo (en funcionamiento) - ¿no es por eso que funciona bien? –

+0

No, no hay otro ciclo. Solo hay un hilo que bloquea la cola y encola las órdenes y luego establece NewOrderEvent. –

Respuesta

4

parece bastante thr ead-safe para mí, WaitAny() simplemente se completará de inmediato porque el evento ya está configurado. Eso es no un problema.

No rompa la sincronización de subprocesos que funcione. Pero si quieres una ratonera mejor, podrías considerar la BlockingQueue de Joe Duffy en este magazine article. Una versión más general está disponible en .NET 4.0, System.Collections.Concurrent.BlockingCollection con ConcurrentQueue como una implementación práctica del mismo.

+0

Pero como se trata de un AutoResetEvent, si el otro hilo establece el evento antes de que tenga la oportunidad de esperar, no tendré ningún problema. Estoy de acuerdo en que si fue un evento manualResetEvent, es una historia diferente. –

+0

Whaaa ... está en 4.0? ¿Cómo es que no recibí la nota? Aprende algo nuevo todos los días. –

+0

@Michael: así no es como funciona ARE, la llamada de espera se restablece al salir. No se reinicia en la entrada, luego espera. –

0

Sólo se debe utilizar el WaitAny para eso, y asegurarse de que se señaliza en cada nuevo pedido agregado a la colección PendingOrders:

while (WaitHandle.WaitAny(CommandEventArr) != 1)) 
{ 
    lock (PendingOrders) 
    { 
     if (PendingOrders.Count > 0) 
     { 
      fbo = PendingOrders.Dequeue(); 
     } 
     else 
     { 
      fbo = null; 

      //Only if you want to exit when there are no more PendingOrders 
      return; 
     } 
    } 

    // Do Some Work if fbo is != null 
} 
+0

Si se ponen en cola dos elementos mientras uno está ejecutando esto, solo Dequeuerá uno de ellos y luego esperará a que otro se ponga en cola. –

+0

Ese es exactamente el problema que me llevó a usar la verificación Count> 0 en primer lugar. Si algún otro hilo encola un elemento y establece el evento mientras estoy en el ciclo while, lo extrañaré. –

2

Usando eventos manuales ...

ManualResetEvent[] CommandEventArr = new ManualResetEvent[] { NewOrderEvent, ExitEvent }; 

while ((WaitHandle.WaitAny(CommandEventArr) != 1)) 
{ 
    lock (PendingOrders) 
    { 
     if (PendingOrders.Count > 0) 
     { 
      fbo = PendingOrders.Dequeue(); 
     } 
     else 
     { 
      fbo = null; 
      NewOrderEvent.Reset(); 
     } 
    } 
} 

Entonces es necesario asegurarse de una cerradura en el lado enqueue así:

lock (PendingOrders) 
    { 
     PendingOrders.Enqueue(obj); 
     NewOrderEvent.Set(); 
    } 
+0

Parece que funcionaría. Déjame pensarlo un poco más, pero eso me parece bien. ¡Gracias! –

+0

+1: Solo le he dado una mirada superficial, pero creo que esta solución es de hecho segura. La razón por la cual este código es más fácil de entender es porque no prueba ninguna de esas estrategias extrañas de bloqueo (que son ** enormes ** banderas rojas). –

+0

Debo aclarar que esto es seguro solo si hay un solo hilo haciendo el enqueueing. Tener dos o más subprocesos en secuencia puede llevar a una situación en la que la cola tiene un elemento pero el ARE no está establecido. –

3

Usted está correcto. El código no es seguro para subprocesos. Pero no por la razón que piensas.

El AutoResetEvent está bien. Sin embargo, solo porque adquiere un bloqueo y vuelve a probar PendingOrders.Count. El quid de la cuestión es que estás llamando a PendingOrders.Count fuera de un bloqueo. Como la clase Queue no es segura para subprocesos, su código no es seguro para subprocesos ... punto.

Ahora, en realidad, probablemente nunca tenga un problema con esto por dos razones. En primer lugar, la propiedad Queue.Count está diseñada casi con certeza para nunca dejar el objeto en un estado medio cocido. Después de todo, probablemente solo devuelva una variable de instancia. En segundo lugar, la falta de una barrera de memoria en esa lectura no tendrá un impacto significativo en el contexto más amplio de su código. Lo peor que sucederá es que obtendrá una lectura obsoleta en una iteración del ciclo y luego el bloqueo adquirido creará una barrera de memoria y una nueva lectura tendrá lugar en la próxima iteración. Asumo aquí que solo hay un hilo en cola de elementos. Las cosas cambian considerablemente si hay 2 o más.

Sin embargo, permítanme dejar esto perfectamente claro. No tiene garantía de que PendingOrders.Count no altere el estado del objeto durante su ejecución. Y debido a que no está envuelto en un bloqueo, otro hilo podría iniciar una operación mientras aún se encuentre en ese estado de respaldo medio.

+0

De hecho, Queue.Count parece devolver un campo int en la implementación actual y no está marcado como volátil. –

+0

Hola Brian, en cuanto a tu primer punto, estoy de acuerdo. Una implementación arbitraria de Queue podría hacer cualquier cosa en la propiedad Count, incluida la iteración a través de todos los elementos. Debería cambiar la pregunta para decir que podemos suponer que Queue .Count solo devuelve una variable de instancia (una lectura no volátil no entrelazada). –

+0

Estoy de acuerdo en que podría obtener una lectura obsoleta. Es decir, el valor real de Count es 1, pero mi lectura de Count en el estado while loop devuelve 0. Pero creo que eso estaría bien todavía. La verificación Count> 0 solo está ahí para detectar el caso en el que los productores ponen en cola las órdenes M al mismo tiempo, por lo que no solo proceso una de las órdenes. Creo que tienes razón, que la barrera de la memoria de la cerradura me salvará. Solo trato de pensar si hay algún caso en la práctica donde haya un problema. (Incluyendo tener N hilos de productor, suponiendo que cada uno bloquea la cola antes de enqueueing). –