¿Cómo esperar a que el hilo termine con .NET?

178

Nunca antes he usado subprocesos en C # donde necesito tener dos subprocesos, así como el subproceso principal de la interfaz de usuario. Básicamente, tengo lo siguiente.

public void StartTheActions()
{
  //Starting thread 1....
  Thread t1 = new Thread(new ThreadStart(action1));
  t1.Start();

  // Now, I want for the main thread (which is calling `StartTheActions` method) 
  // to wait for `t1` to finish. I've created an event in `action1` for this. 
  // The I wish `t2` to start...

  Thread t2 = new Thread(new ThreadStart(action2));
  t2.Start();
}

Entonces, esencialmente, mi pregunta es cómo hacer que un hilo espere a que termine otro. ¿Cuál es la mejor manera de hacer esto?

Maxim Zaslavsky
fuente
44
Si "... nunca ha usado subprocesos antes ...", puede considerar algunas alternativas más simples como el patrón * Async ().
El
44
Si solo está esperando que el hilo 1 termine de todos modos, ¿por qué no está llamando a ese método sincrónicamente?
Svish
13
¿Cuál es el punto de usar hilos cuando estás procesando de forma lineal?
John
1
@John, tiene mucho sentido para mí que hay muchos usos para escindir un hilo de fondo que funciona mientras el usuario trabaja. Además, ¿no es tu pregunta la misma que la anterior?
user34660
La respuesta de Rotem , usando backgroundworker para un uso fácil, es muy simple.
Kamil

Respuestas:

264

Puedo ver 5 opciones disponibles:

1. Hilo. Únete

Como con la respuesta de Mitch. Pero esto bloqueará su hilo de interfaz de usuario, sin embargo, obtendrá un tiempo de espera incorporado para usted.


2. Use un WaitHandle

ManualResetEventes WaitHandlecomo sugirió jrista.

Una cosa a tener en cuenta es que si desea esperar varios subprocesos, WaitHandle.WaitAll()no funcionará de forma predeterminada, ya que necesita un subproceso MTA. Puede evitar esto marcando su Main()método con MTAThread, sin embargo, esto bloquea su mensaje y no es recomendable por lo que he leído.


3. Dispara un evento

Vea esta página de Jon Skeet sobre eventos y subprocesos múltiples, es posible que un evento pueda quedar sin suscripción entre el ify el EventName(this,EventArgs.Empty)- me ha sucedido antes.

(Ojalá estas compilación, no lo he probado)

public class Form1 : Form
{
    int _count;

    void ButtonClick(object sender, EventArgs e)
    {
        ThreadWorker worker = new ThreadWorker();
        worker.ThreadDone += HandleThreadDone;

        Thread thread1 = new Thread(worker.Run);
        thread1.Start();

        _count = 1;
    }

    void HandleThreadDone(object sender, EventArgs e)
    {
        // You should get the idea this is just an example
        if (_count == 1)
        {
            ThreadWorker worker = new ThreadWorker();
            worker.ThreadDone += HandleThreadDone;

            Thread thread2 = new Thread(worker.Run);
            thread2.Start();

            _count++;
        }
    }

    class ThreadWorker
    {
        public event EventHandler ThreadDone;

        public void Run()
        {
            // Do a task

            if (ThreadDone != null)
                ThreadDone(this, EventArgs.Empty);
        }
    }
}

4. Use un delegado

public class Form1 : Form
{
    int _count;

    void ButtonClick(object sender, EventArgs e)
    {
        ThreadWorker worker = new ThreadWorker();

        Thread thread1 = new Thread(worker.Run);
        thread1.Start(HandleThreadDone);

        _count = 1;
    }

    void HandleThreadDone()
    {
        // As before - just a simple example
        if (_count == 1)
        {
            ThreadWorker worker = new ThreadWorker();

            Thread thread2 = new Thread(worker.Run);
            thread2.Start(HandleThreadDone);

            _count++;
        }
    }

    class ThreadWorker
    {
        // Switch to your favourite Action<T> or Func<T>
        public void Run(object state)
        {
            // Do a task

            Action completeAction = (Action)state;
            completeAction.Invoke();
        }
    }
}

Si usa el método _count, podría ser una idea (para estar seguro) incrementarlo usando

Interlocked.Increment(ref _count)

Me interesaría saber la diferencia entre usar delegados y eventos para la notificación de subprocesos, la única diferencia que sé es que los eventos se llaman sincrónicamente.


5. Hazlo asincrónicamente

La respuesta a esta pregunta tiene una descripción muy clara de sus opciones con este método.


Delegado / Eventos en el hilo equivocado

La forma de hacer eventos / delegar significará que su método de controlador de eventos está en thread1 / thread2 no en el hilo principal de la interfaz de usuario , por lo que deberá volver a la derecha en la parte superior de los métodos HandleThreadDone:

// Delegate example
if (InvokeRequired)
{
    Invoke(new Action(HandleThreadDone));
    return;
}
Chris S
fuente
61

Añadir

t1.Join();    // Wait until thread t1 finishes

después de iniciarlo, ¡pero eso no logrará mucho, ya que es esencialmente el mismo resultado que ejecutar en el hilo principal!

Puedo recomendar leer el libro electrónico gratuito Threading in C # de Joe Albahari , si desea obtener una comprensión de los hilos en .NET.

Trigo Mitch
fuente
44
Aunque Joines literalmente lo que parece haber pedido el autor de la pregunta, en general puede ser extremadamente malo. Una llamada a Joincolgará el hilo desde el que se está haciendo esto. Si ese es el hilo principal de la GUI, ¡es MALO ! Como usuario, detesto activamente las aplicaciones que parecen funcionar de esta manera. Así que por favor vea todas las otras respuestas a esta pregunta y stackoverflow.com/questions/1221374/…
peSHIr
1
Estoy de acuerdo en que, en general, Join () es malo. Quizás no hice eso lo suficientemente obvio en mi respuesta.
Mitch Wheat
2
Chicos, una talla no sirve para todos . Hay situaciones en las que uno realmente necesita estar seguro de que el hilo ha terminado su trabajo: considere que el hilo procesa los datos, que están a punto de ser cambiados. En tal caso, la notificación de que el hilo se cancele con gracia y esperar hasta que termine (especialmente, cuando un paso se procesa muy rápidamente) está totalmente justificado por la OMI. Prefiero decir que Join es malo (en términos de preguntas frecuentes de C ++), es decir. no se utilizará a menos que sea realmente necesario.
Asustado el
55
Quiero decir claramente, que Join es una herramienta, que puede ser útil, a pesar del hecho, que a menudo se usa mal. Hay algunas situaciones en las que simplemente funcionará sin efectos secundarios innecesarios (como detener el hilo principal de la GUI durante un período de tiempo notable).
Asustado el
2
Unirse es una herramienta? Creo que encontrarás que es un método.
Mitch Wheat
32

Las dos respuestas anteriores son geniales y funcionarán para escenarios simples. Sin embargo, hay otras formas de sincronizar hilos. Lo siguiente también funcionará:

public void StartTheActions()
{
    ManualResetEvent syncEvent = new ManualResetEvent(false);

    Thread t1 = new Thread(
        () =>
        {
            // Do some work...
            syncEvent.Set();
        }
    );
    t1.Start();

    Thread t2 = new Thread(
        () =>
        {
            syncEvent.WaitOne();

            // Do some work...
        }
    );
    t2.Start();
}

ManualResetEvent es uno de los diversos WaitHandle que ofrece .NET Framework. Pueden proporcionar capacidades de sincronización de subprocesos mucho más ricas que las herramientas simples pero muy comunes como lock () / Monitor, Thread.Join, etc. También se pueden usar para sincronizar más de dos subprocesos, lo que permite escenarios complejos como un subproceso 'maestro' que coordina múltiples subprocesos 'secundarios', múltiples procesos concurrentes que dependen de varias etapas entre sí para sincronizarse, etc.

jrista
fuente
30

Si utiliza desde .NET 4, este ejemplo puede ayudarlo a:

class Program
{
    static void Main(string[] args)
    {
        Task task1 = Task.Factory.StartNew(() => doStuff());
        Task task2 = Task.Factory.StartNew(() => doStuff());
        Task task3 = Task.Factory.StartNew(() => doStuff());
        Task.WaitAll(task1, task2, task3);
        Console.WriteLine("All threads complete");
    }

    static void doStuff()
    {
        //do stuff here
    }
}

de: https://stackoverflow.com/a/4190969/1676736

Sayed Abolfazl Fatemi
fuente
8
No mencionaste nada sobre Hilos en tu respuesta. La pregunta es sobre hilos, no tareas. Los dos no son lo mismo.
Suamere
77
Llegué con una pregunta similar (y nivel de conocimiento) al póster original y esta respuesta fue muy valiosa para mí: las tareas son mucho más adecuadas para lo que estoy haciendo y si no hubiera encontrado esta respuesta, habría escrito mi propio grupo de hilos terribles.
Chris Rae
1
@ChrisRae, así que esto debería ser un comentario en la pregunta original, no una respuesta como esta.
Jaime Hablutzel
Como se trata de esta respuesta específica, creo que probablemente tenga más sentido aquí.
Chris Rae el
1
Como dijo @Suamere, esta respuesta no tiene ninguna relación con la pregunta de OP.
Glenn Slayden
4

Quisiera que su hilo principal pase un método de devolución de llamada a su primer hilo, y cuando termine, invocará el método de devolución de llamada en el hilo principal, que puede iniciar el segundo hilo. Esto evita que su hilo principal se cuelgue mientras espera un Join o Waithandle. Pasar métodos como delegados es algo útil para aprender con C # de todos modos.

Ed Power
fuente
0

Prueba esto:

List<Thread> myThreads = new List<Thread>();

foreach (Thread curThread in myThreads)
{
    curThread.Start();
}

foreach (Thread curThread in myThreads)
{
    curThread.Join();
}
Wendell Tagsip
fuente
0

Posiblemente para ayudar a otros, pasé bastante tiempo buscando una solución como la que se me ocurrió. Entonces tomé un enfoque un poco diferente. Hay una opción de contador arriba, solo la apliqué un poco diferente. Estaba hilando numerosos hilos e incrementé un contador y disminuí un contador cuando un hilo comenzó y se detuvo. Luego, en el método principal, quería pausar y esperar a que se completaran los hilos que 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/

Adán
fuente
44
Esto se llama espera ocupada. Sí, funciona y, a veces, es la mejor solución, pero debes evitarlo si es posible porque desperdicia tiempo de CPU
MobileMon
@MobileMon es más una guía que una regla. En este caso, dado que ese bucle podría desperdiciar el 0.00000001% de la CPU y el OP está codificando en C #, reemplazar esto con algo 'más eficiente' sería una completa pérdida de tiempo. La primera regla de optimización es: no lo hagas. Mide primero.
Spike0xff
0

Cuando quiero que la IU pueda actualizar su pantalla mientras espero que se complete una tarea, uso un ciclo while que prueba IsAlive en el hilo:

    Thread t = new Thread(() => someMethod(parameters));
    t.Start();
    while (t.IsAlive)
    {
        Thread.Sleep(500);
        Application.DoEvents();
    }
Mark Emerson
fuente
-1

Aquí hay un ejemplo simple que espera a que termine una banda de rodadura, dentro de la misma clase. También realiza una llamada a otra clase en el mismo espacio de nombres. Incluí las declaraciones de "uso" para que pueda ejecutarse como Winform siempre que cree el botón1.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;

namespace ClassCrossCall
{

 public partial class Form1 : Form
 {
  int number = 0; // this is an intentional problem, included for demonstration purposes
  public Form1()
  {
   InitializeComponent();
  }
  private void Form1_Load(object sender, EventArgs e)
  {
   button1.Text="Initialized";
  }
  private void button1_Click(object sender, EventArgs e)
  {
   button1.Text="Clicked";
   button1.Refresh();
   Thread.Sleep(400);
   List<Task> taskList = new List<Task>();
   taskList.Add(Task.Factory.StartNew(() => update_thread(2000)));
   taskList.Add(Task.Factory.StartNew(() => update_thread(4000)));
   Task.WaitAll(taskList.ToArray());
   worker.update_button(this,number);
  }
  public void update_thread(int ms)
  {
   // It's important to check the scope of all variables
   number=ms; // this could be either 2000 or 4000.  Race condition.
   Thread.Sleep(ms);
  }
 }

 class worker
 {
  public static void update_button(Form1 form, int number)
  {
   form.button1.Text=$"{number}";
  }
 }
}
usuario3029478
fuente