2009-12-27 8 views
17

Quizás sea demasiado tarde en la noche, pero no puedo pensar en una buena manera de hacerlo..NET Reverse Semaphore?

He comenzado un montón de descargas asíncronas, y quiero esperar hasta que todas se completen antes de que el programa finalice. Esto me lleva a pensar que debo incrementar algo cuando comienza una descarga, y decrementarlo cuando termine. Pero entonces ¿cómo espero hasta que el recuento sea 0 nuevamente?

Semaphores tipo de trabajo en la forma opuesta en la que bloquea cuando no hay recursos disponibles, no cuando están todos disponibles (bloques cuando el recuento es 0, en lugar de distinto de cero).

+0

bloqueo en este olor divertido –

+0

@Ori: ¿Por qué? MSDN dice que es una forma común de hacerlo: http://msdn.microsoft.com/en-us/library/c5kehkcz(VS.71).aspx – mpen

+0

@Mark: tenga mucho cuidado con el uso de documentación antigua. Su página era la versión de .NET 1.1, cuando no sabíamos mejor. Ver http://msdn.microsoft.com/en-us/library/c5kehkcz.aspx. –

Respuesta

13

Mira la clase CountdownLatch en este magazine article.

Actualización: ahora cubierto por el marco desde la versión 4.0, CountdownEvent class.

+7

Un artículo bastante impresionante de principio a fin. – spender

+0

@spender - ¡Así lo esperarías de Joe Duffy! –

+0

Ese 'BoundedBuffer' parece bastante útil, pero el' CountdownLatch' no era exactamente lo que estaba buscando. Quiero poder aumentarlo también (lo que debería borrar la señal). – mpen

4

Bueno ... puede arrebatar todos los contadores de semáforos en el hilo principal hacia atrás con el fin de bloques cuando el número es 0, en lugar de no-cero.

MODIFICADO: Aquí asumió 3 cosas:

  • Mientras que el programa se está ejecutando, un nuevo trabajo de descarga puede comenzar en cualquier momento.
  • Al salir del programa, no habrá más descargas nuevas que necesiten atención.
  • Al salir del programa, tiene que esperar a que los todos los archivos para terminar la descarga

Así que aquí está mi solución, revisada:

Inicializa el semáforo con un contador lo suficientemente grande como para que nunca se pulsa el máximo (que podría ser simplemente 100 o 10 dependiendo de su situación):

var maxDownloads = 1000; 
_semaphore = new Semaphore(0, maxDownloads); 

Luego, en cada descarga, comienza con WaitOne() antes de iniciar la descarga de modo que en caso de salir del programa, sin descargas pueden empezar .

if (_semaphore.WaitOne()) 
    /* proceeds with downloads */ 
else 
    /* we're terminating */ 

A continuación, en la terminación de descarga, liberar un contador (si hubiéramos adquirido uno):

finally { _semaphore.Release(1); } 

y luego en el evento "Salir", consumen todos los contadores en el semáforo:

for (var i = 0; i < maxDownloads; i++) 
    _semaphore.WaitOne(); 

// all downloads are finished by this point. 

...

+0

Me temo que no entiendo del todo. ¿Qué pasa si no sé el número máximo de descargas? Y si pongo '_semahpore.WaitOne();' * antes de * inicio la descarga, y el recuento inicial es 0, ¿no esperará indefinidamente? – mpen

+0

Debe agregar esta oración a su pregunta: "¿Qué sucede si no conozco el número máximo de descargas?" – chakrit

+0

@Mark Usted * debe * saber la cantidad de operaciones de descarga asíncrona que se realizan simultáneamente antes del evento de "bloqueo" que desea; de lo contrario, si una nueva descarga puede surgir en el medio o durante el evento de bloqueo, necesita una sistema complejo más que un simple semáforo o waithandles. Necesitarás una combinación de ellos. – chakrit

0

Para cada hilo de empezar Interbloqueo.Incremento un contador. Y para cada devolución de llamada en acabado de hilo, disminuirlo.

Luego haga un ciclo con un Thread.Sleep (10) o algo así hasta que el conteo llegue a cero.

+9

Los programas reales no duermen. Solo las demos lo hacen. –

+0

@Henk Creo que se refería a: Los programas reales no están ocupados: esperen. Solo las demos lo hacen. – chakrit

+0

Y tengo que estar de acuerdo. Código descuidado de mi parte. Me gustan las soluciones de @ chakrit y @ ssg. Por cierto, ¿alguien tiene información sobre cómo se implementa WaitOneNative dentro de la estructura? –

7

Parece que System.Threading.WaitHandle.WaitAll podría ser un ajuste muy bueno:

Espera a que todos los elementos de la matriz especificada para recibir una señal.

+0

Esto parece una solución viable, aunque parece un poco innecesariamente complejo ... empujar objetos en una matriz y configurar/restablecerlos individualmente, cuando un simple contador debe hacer ... – mpen

10

In.NET 4 hay un tipo especial para ese propósito CountdownEvent.

O puede construir algo similar a sí mismo de esta manera:

const int workItemsCount = 10; 
// Set remaining work items count to initial work items count 
int remainingWorkItems = workItemsCount; 

using (var countDownEvent = new ManualResetEvent(false)) 
{ 
    for (int i = 0; i < workItemsCount; i++) 
    { 
     ThreadPool.QueueUserWorkItem(delegate 
             { 
              // Work item body 
              // At the end signal event 
              if (Interlocked.Decrement(ref remainingWorkItems) == 0) 
               countDownEvent.Set(); 
             }); 
    } 
    // Wait for all work items to complete 
    countDownEvent.WaitOne(); 
} 
+1

Aw snap ... that 'CountdownEvent' se veía tan bien hasta que lo probé. No le permite iniciar el recuento en 0 e incrementarlo porque ya está señalizado. – mpen

+0

@Mark ¿podría explicar el problema con 'CountdownEvent' con mayor claridad? No veo el problema con eso. – Jez

+0

@Jez: Han pasado más de 3 años desde que dejé el comentario, pero creo que el problema que tenía con el 'CountdownEvent' es que se dispara tan pronto como llega a 0, y quería comenzar en 0 y aumentar como Agregué elementos, pero no lo tengo disparar hasta que llegue a 0 de nuevo. Creo. – mpen

0

Aquí es mi aplicación C# 2.0 de CountdownLatch:

public class CountdownLatch 
{ 
    private int m_count; 
    private EventWaitHandle m_waitHandle = new EventWaitHandle(true, EventResetMode.ManualReset); 

    public CountdownLatch() 
    { 
    } 

    public void Increment() 
    { 
     int count = Interlocked.Increment(ref m_count); 
     if (count == 1) 
     { 
      m_waitHandle.Reset(); 
     } 
    } 

    public void Add(int value) 
    { 
     int count = Interlocked.Add(ref m_count, value); 
     if (count == value) 
     { 
      m_waitHandle.Reset(); 
     } 
    } 

    public void Decrement() 
    { 
     int count = Interlocked.Decrement(ref m_count); 
     if (m_count == 0) 
     { 
      m_waitHandle.Set(); 
     } 
     else if (count < 0) 
     { 
      throw new InvalidOperationException("Count must be greater than or equal to 0"); 
     } 
    } 

    public void WaitUntilZero() 
    { 
     m_waitHandle.WaitOne(); 
    } 
} 
0

Sobre la base de las sugerencias aquí, esto es lo que ocurrió. Además de esperar hasta que el conteo sea 0, dormirá si genera demasiados hilos (count> max). Advertencia: Esto no ha sido probado completamente.

public class ThreadCounter 
{ 
    #region Variables 
    private int currentCount, maxCount; 
    private ManualResetEvent eqZeroEvent; 
    private object instanceLock = new object(); 
    #endregion 

    #region Properties 
    public int CurrentCount 
    { 
     get 
     { 
      return currentCount; 
     } 
     set 
     { 
      lock (instanceLock) 
      { 
       currentCount = value; 
       AdjustZeroEvent(); 
       AdjustMaxEvent(); 
      } 
     } 
    } 

    public int MaxCount 
    { 
     get 
     { 
      return maxCount; 
     } 
     set 
     { 
      lock (instanceLock) 
      { 
       maxCount = value; 
       AdjustMaxEvent(); 
      } 
     } 
    } 
    #endregion 

    #region Constructors 
    public ThreadCounter() : this(0) { } 
    public ThreadCounter(int initialCount) : this(initialCount, int.MaxValue) { } 
    public ThreadCounter(int initialCount, int maximumCount) 
    { 
     currentCount = initialCount; 
     maxCount = maximumCount; 
     eqZeroEvent = currentCount == 0 ? new ManualResetEvent(true) : new ManualResetEvent(false); 
    } 
    #endregion 

    #region Public Methods 
    public void Increment() 
    { 
     ++CurrentCount; 
    } 

    public void Decrement() 
    { 
     --CurrentCount; 
    } 

    public void WaitUntilZero() 
    { 
     eqZeroEvent.WaitOne(); 
    } 
    #endregion 

    #region Private Methods 
    private void AdjustZeroEvent() 
    { 
     if (currentCount == 0) eqZeroEvent.Set(); 
     else eqZeroEvent.Reset(); 
    } 

    private void AdjustMaxEvent() 
    { 
     if (currentCount <= maxCount) Monitor.Pulse(instanceLock); 
     else do { Monitor.Wait(instanceLock); } while (currentCount > maxCount); 
    } 
    #endregion 
} 
1

tuve un problema similar en el que necesitaba para reiniciar un servidor en algún evento, pero tuvo que esperar a que todas las solicitudes abiertas para terminar antes de matarlo.

he utilizado la CountdownEvent class en el arranque del servidor para inicializar con 1, y dentro de cada petición que hago:

try 
{ 
    counter.AddCount(); 
    //do request stuff 
} 
finally 
{ 
    counter.Signal(); 
} 

Y al recibir el ResetEvent dé la señal del contador una vez para eliminar la partida 1, y esperar solicitudes en vivo para indicar que están hechas.

void OnResetEvent() 
{ 
    counter.Signal(); 
    counter.Wait(); 
    ResetServer(); 
    //counter.Reset(); //if you want to reset everything again. 
} 

Básicamente inicializar la CountdownEvent con uno, para que sea de una manera no señalizado estado, y con cada addCount llame usted está aumentando el contador, y con cada señal de llamada que están disminuyendo ella, permaneciendo siempre por encima de 1. En el hilo de espera primero lo indicas una vez para disminuir el valor inicial de 1 a 0, y si no hay subprocesos ejecutando Wail() inmediatamente se detendrá el bloqueo, pero si hay otros subprocesos que todavía están en ejecución, el hilo de espera esperará hasta ellos señalan. Tenga cuidado, una vez que el contador llegue a 0, todas las llamadas AddCount posteriores arrojarán una excepción, primero debe reiniciar el contador.