2010-03-27 18 views
50

solo quiero algunos consejos sobre "mejores prácticas" con respecto a tareas de subprocesos múltiples.Engendrar varios subprocesos para el trabajo y luego esperar hasta que todos terminen

como ejemplo, tenemos una aplicación C# que al inicio lee datos de varias tablas de "tipo" en nuestra base de datos y almacena la información en una colección que pasamos por la aplicación. esto nos impide golpear la base de datos cada vez que se requiere esta información.

en el momento en que la aplicación está leyendo datos de 10 tablas sincrónicamente. Realmente me gustaría tener la aplicación leída de cada tabla en un hilo diferente, todo funcionando en paralelo. la aplicación esperará a que se completen todos los subprocesos antes de continuar con el inicio de la aplicación.

He examinado BackGroundWorker pero solo quiero algunos consejos para lograr lo anterior.

  1. que hace el método suena lógico con el fin de acelerar el tiempo de arranque de nuestra aplicación
  2. ¿Cómo podemos manejar todos los hilos teniendo en cuenta que el trabajo de cada hilo es independiente el uno del otro, sólo tenemos que espere a que se completen todos los hilos antes de continuar.

espero con interés algunas respuestas

+1

Tome un vistazo a la biblioteca de tareas parrallel, esto realmente simplificar las cosas para usted. – TimothyP

+0

¿te refieres a la Biblioteca de tareas paralelas (TPL)? Lo investigaré – pharoc

+1

TPL es fantástico para esto, pero requiere al menos .NET 3.5sp1 (si instala Rx) o .NET 4 RC (incorporado en el marco) para usar .... Ha sugerido que necesita un .NET 2 solución. –

Respuesta

7

Si no estás en .NET 4.0 a continuación, se puede utilizar una lista < ManualResetEvent>, uno para cada hilo y Wait para que sean Set. Para esperar múltiples hilos, puede considerar usar WaitAll, pero tenga cuidado con el límite de 64 identificadores de espera. Si necesita más que esto, puede recorrerlos y esperar cada uno individualmente.

Si desea una experiencia de inicio más rápida, probablemente no necesite esperar a que se lean todos los datos durante el inicio. Simplemente muestre la GUI y cualquier información que falta se puede mostrar atenuada con algún tipo de ícono de "Actualización ..." o similar. Cuando ingrese la información, solo active un evento para actualizar la GUI. Podría haber muchas operaciones que el usuario puede comenzar a realizar incluso antes de que todos los datos de todas las tablas se leen en

+0

¡ManualResetEvent es mi herramienta de sincronización favorita! – Polaris878

+1

Marca: Esto se puede hacer con un solo ManualResetEvent y la clase Interlocked. Ver mi respuesta a continuación para más detalles. –

6

Si te sientes aventurero puede utilizar C# 4.0 y la Biblioteca paralelo de tareas:.

Parallel.ForEach(jobList, curJob => { 
    curJob.Process() 
}); 
+0

mmmmm thx, pero C# 4.0 parece muy lejos en el extremo derecho de la curva para aplicaciones de negocios. podría tener que encontrar una solución C# 2.0. – pharoc

1

Suponiendo que los hilos del lector de la base de datos regresen tan pronto como terminen, simplemente puede llamar a Thread.Join en los diez hilos a su vez desde el hilo de inicio.

9

Si tiene más de 64 controladores de espera para un hilo STA como dice Mark. podría crear una lista con sus hilos y esperar a que todo se complete en un segundo ciclo.

//check that all threads have completed. 
foreach (Thread thread in threadList) 
{ 
    thread.Join(); 

} 
+0

+1 A veces, un simple 'Join' funciona muy bien. –

2

Simplemente por diversión, lo que @Reed ha hecho, con Monitor. : P

class Program 
{ 
    static void Main(string[] args) 
    { 
     int numThreads = 10; 
     int toProcess = numThreads; 
     object syncRoot = new object(); 

     // Start workers. 
     for (int i = 0; i < numThreads; i++) 
     { 
      new Thread(delegate() 
      { 
       Console.WriteLine(Thread.CurrentThread.ManagedThreadId); 
       // If we're the last thread, signal 
       if (Interlocked.Decrement(ref toProcess) == 0) 
       { 
        lock (syncRoot) 
        { 
         Monitor.Pulse(syncRoot); 
        } 
       } 
      }).Start(); 
     } 

     // Wait for workers. 
     lock (syncRoot) 
     { 
      if (toProcess > 0) 
      { 
       Monitor.Wait(syncRoot); 
      } 
     } 

     Console.WriteLine("Finished."); 
    } 
} 
+0

@andras: Puse otra modificación de esto usando un solo ManualResetEvent: es más agradable porque elimina por completo la necesidad de bloquear el procesamiento, por lo que escala mucho mejor para contar con más trabajadores. La opción de semáforo es similar, aunque personalmente pienso que es más complicado que mi respuesta a continuación. –

+0

@Reed: no estaba contento con la solución de semáforo. (Incluso lo he borrado de la respuesta en un punto). Incluso la solución basada en Monitor parecía mucho más agradable. Tu solución es mejor en todos los aspectos. (... y en cuanto al rendimiento, es el rey ....) :) –

+0

@Reed: corregido. Gracias. :) –

88

Mi preferencia para esto es para manejar esto a través de un único WaitHandle, y utilizar empotrado a evitar el bloqueo en un contador:

class Program 
{ 
    static void Main(string[] args) 
    { 
     int numThreads = 10; 
     ManualResetEvent resetEvent = new ManualResetEvent(false); 
     int toProcess = numThreads; 

     // Start workers. 
     for (int i = 0; i < numThreads; i++) 
     { 
      new Thread(delegate() 
      { 
       Console.WriteLine(Thread.CurrentThread.ManagedThreadId); 
       // If we're the last thread, signal 
       if (Interlocked.Decrement(ref toProcess) == 0) 
        resetEvent.Set(); 
      }).Start(); 
     } 

     // Wait for workers. 
     resetEvent.WaitOne(); 
     Console.WriteLine("Finished."); 
    } 
} 

Esto funciona bien, y las escalas a cualquier número de hilos procesamiento, sin introducir bloqueo.

+1

Tienes mucha razón. Todas las otras soluciones probablemente despertarían falsamente el hilo de espera o al menos lo colocarían en la lista de espera. Algunas veces esto debe evitarse, y si no, al menos * debería * evitarse. Yo lo eliminaré. : P –

+0

@andras: (por cierto, con el comentario de "bloqueo al final del procesamiento" en la otra respuesta, incluso el bloqueo puede crear un efecto de "cola" que retrasa las cosas. Con 10 hilos, esto no es un problema , pero si intenta sincronizar contra cientos, o si está reprogramando sobre la marcha, en realidad puede ralentizar las cosas) –

+0

@Reed: si estamos realmente quisquillosos, :) se puede argumentar que 'Interlocked' crear un efecto de "cola" también. (No hay dos CPU-s que puedan contener los contenidos de la misma dirección en su caché exclusivamente para escribir al mismo tiempo. Deben invalidar la línea de caché y pasar el nuevo contenido a través de la memoria principal o la comunicación entre cachés. Esto crea un efecto de "cola" si actualiza la misma ubicación de memoria desde diferentes CPUs - las escrituras deben ser serializadas.) –

0

Si está utilizando .NET 3.5 o inferior, puede usar una matriz de AsyncResult o BackgroundWorker y contar cuántos hilos han regresado (simplemente no olvide disminuir el contador con operaciones entrelazadas) (vea http://www.albahari.com/threading/ como referencia).

Si está utilizando .NET 4.0, un paralelo para es el enfoque más simple.

+0

cualquier muestra de código fuente completo con .NET 4.0? – Kiquenet

+0

@Kiquenet Nada listo a lo que actualmente tenga acceso. No estoy seguro de que este sea el sitio correcto para solicitar ejemplos completos de código. –

23

Me gusta la solución de @ Reed. Otra forma de lograr lo mismo en .NET 4.0 sería usar un CountdownEvent.

class Program 
{ 
    static void Main(string[] args) 
    { 
     var numThreads = 10; 
     var countdownEvent = new CountdownEvent(numThreads); 

     // Start workers. 
     for (var i = 0; i < numThreads; i++) 
     { 
      new Thread(delegate() 
      { 
       Console.WriteLine(Thread.CurrentThread.ManagedThreadId); 
       // Signal the CountdownEvent. 
       countdownEvent.Signal(); 
      }).Start(); 
     } 

     // Wait for workers. 
     countdownEvent.Wait(); 
     Console.WriteLine("Finished."); 
    } 
} 
+0

Esto es bueno, el único problema es que debes saber cuántos subprocesos habrá antes de entrar en tu ciclo ... así que si tienes lógica con 'continue's en tu ciclo, esto no es tan útil, pero si no necesario, esto se siente de la manera más limpia. –

0

un método más fácil que me gusta usar:

private int ThreadsCount = 100; //initialize threads count 
    private void button1_Click(object sender, EventArgs e) 
    { 
     for (int i = 0; i < ThreadsCount; i++) 
     { 
      Thread t = new Thread(new ThreadStart(myMethod)); 
      t.IsBackground = true; 
      t.Start(); 
     } 
    } 

    private void myMethod() 
    { 
     //everytime a thread finishes executing decrease the threads count 
     ThreadsCount = ThreadsCount - 1; 

     if (ThreadsCount < 1) 
     { 
      //if all threads finished executing do whatever you wanna do here.. 
      MessageBox.Show("Finished Executing all threads!!!"); 
     } 
    } 
2

Éstos son dos patrones de espera en múltiples operaciones paralelas. El truco es que debes tratar tu hilo principal como si fuera una de las operaciones paralelas también. De lo contrario, existe una condición de carrera sutil entre la señalización de finalización en los hilos de trabajo y la espera de esa señal del hilo principal.

int threadCount = 1; 
ManualResetEvent finished = new ManualResetEvent(false); 
for (int i = 0; i < NUM_WORK_ITEMS; i++) 
{ 
    Interlocked.Increment(ref threadCount); 
    ThreadPool.QueueUserWorkItem(delegate 
    { 
     try 
     { 
      // do work 
     } 
     finally 
     { 
      if (Interlocked.Decrement(ref threadCount) == 0) finished.Set(); 
     } 
    }); 
} 
if (Interlocked.Decrement(ref threadCount) == 0) finished.Set(); 
finished.WaitOne(); 

Como una preferencia personal me gusta utilizar la clase CountdownEvent para hacer el recuento para mí que está disponible en .NET 4.0.

var finished = new CountdownEvent(1); 
for (int i = 0; i < NUM_WORK_ITEMS; i++) 
{ 
    finished.AddCount(); 
    ThreadPool.QueueUserWorkItem(delegate 
    { 
     try 
     { 
      // do work 
     } 
     finally 
     { 
     finished.Signal(); 
     } 
    }); 
} 
finished.Signal(); 
finished.Wait(); 

Los ejemplos anteriores usan la ThreadPool, pero se puede cambiar que por cualquier mecanismo de enhebrado lo prefiere.

0

Publicando para tal vez ayudar a otros, pasé bastante tiempo buscando una solución como la que se me ocurrió. Así que tomé un enfoque diferente. Estaba generando numerosos hilos e incrementé un contador y decrementé un contador cuando un hilo comenzaba y se detenía. Luego, en el método principal, quería detenerme y esperar a que terminaran los hilos, lo hice.

while (threadCounter > 0) 
{ 
    Thread.Sleep(500); //Make it pause for half second so that we don’t spin the cpu out of control. 
} 

Documentado en mi blog. http://www.adamthings.com/post/2012/07/11/ensure-threads-have-finished-before-method-continues-in-c/

+0

¿Por qué no funcionó 'Thread.Join()'? –

+0

No pude obtener Thread.Join() para detener la carga de PageLoad, mientras que al hacer un contador simple, pude recorrer hasta que todos los hilos terminaron. – Adam

2

Otra posibilidad con TPL, asumiendo jobs es las colecciones de elementos para procesar, o para ejecutar subthreads:

Task.WaitAll(jobs 
    .Select(job => TaskFactory.StartNew(() => /*run job*/)) 
    .ToArray()); 
Cuestiones relacionadas