Comment attendre que le thread se termine avec .NET?

178

Je n'ai jamais vraiment utilisé le threading auparavant en C # où je dois avoir deux threads, ainsi que le thread principal de l'interface utilisateur. Fondamentalement, j'ai ce qui suit.

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();
}

Donc, essentiellement, ma question est de savoir comment faire attendre un fil pour qu'un autre se termine. Quelle est la meilleure façon de procéder?

Maxim Zaslavsky
la source
4
Si vous "... n'avez jamais utilisé le threading auparavant ...", vous pouvez envisager des alternatives plus simples telles que le modèle * Async ().
Ðаn
4
Si vous attendez juste que le thread 1 se termine de toute façon, pourquoi n'appelez-vous pas simplement cette méthode de manière synchrone?
Svish
13
Quel est l'intérêt d'utiliser des threads lorsque vous traitez de manière linéaire?
John
1
@John, il est tout à fait logique pour moi qu'il existe de nombreuses utilisations pour créer un fil d'arrière-plan qui fonctionne pendant que l'utilisateur travaille. De plus, votre question n'est-elle pas la même que la précédente?
user34660
La réponse de Rotem , en utilisant backgroundworker pour une utilisation facile, est très simple.
Kamil

Réponses:

264

Je peux voir 5 options disponibles:

1. Thread.Join

Comme pour la réponse de Mitch. Mais cela bloquera votre thread d'interface utilisateur, mais vous obtenez un délai d'expiration intégré pour vous.


2. Utilisez un WaitHandle

ManualResetEventest un WaitHandlecomme jrista suggéré.

Une chose à noter est que si vous souhaitez attendre plusieurs threads, WaitHandle.WaitAll()cela ne fonctionnera pas par défaut, car il a besoin d'un thread MTA. Vous pouvez contourner ce problème en marquant votre Main()méthode avec MTAThread- cependant, cela bloque votre pompe de message et n'est pas recommandé d'après ce que j'ai lu.


3. Déclenchez un événement

Voir cette page de Jon Skeet sur les événements et le multi-threading, il est possible qu'un événement puisse devenir désabonné entre le ifet le EventName(this,EventArgs.Empty)- cela m'est déjà arrivé.

(J'espère que ces compilent, je n'ai pas essayé)

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. Utilisez un délégué

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 vous utilisez la méthode _count, il peut être judicieux (pour être sûr) de l'incrémenter en utilisant

Interlocked.Increment(ref _count)

Je serais intéressé de connaître la différence entre l'utilisation de délégués et d'événements pour la notification de thread, la seule différence que je sais est que les événements sont appelés de manière synchrone.


5. Faites-le plutôt de manière asynchrone

La réponse à cette question a une description très claire de vos options avec cette méthode.


Délégué / Événements sur le mauvais fil

La façon de faire les événements / délégués signifie que votre méthode de gestionnaire d'événements est sur thread1 / thread2 et non sur le thread d'interface utilisateur principal , vous devrez donc revenir en haut des méthodes HandleThreadDone:

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

Ajouter

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

après l'avoir démarré, mais cela n'accomplira pas grand-chose car c'est essentiellement le même résultat que de courir sur le thread principal!

Je peux fortement recommandé de lire le livre électronique gratuit Threading in C # de Joe Albahari , si vous souhaitez acquérir une compréhension du threading dans .NET.

Blé Mitch
la source
4
Même si Joinc'est littéralement ce que le demandeur semble avoir demandé, cela peut être extrêmement mauvais en général. Un appel à Joinraccroche le fil à partir duquel cela est fait. Si cela se trouve être le fil principal de l'interface graphique, c'est MAUVAIS ! En tant qu'utilisateur, je déteste activement les applications qui semblent fonctionner de cette façon. Alors s'il vous plaît voir toutes les autres réponses à cette question et stackoverflow.com/questions/1221374/…
peSHIr
1
Je suis d'accord qu'en général Join () est mauvais. Je n'ai peut-être pas rendu cela assez évident dans ma réponse.
Mitch Wheat
2
Les gars, une seule taille ne convient pas à tous . Il y a des situations où il faut vraiment être sûr que le thread a terminé son travail: considérez que le thread traite les données, qui sont sur le point d'être modifiées. Dans un tel cas, aviser le thread d'annuler gracieusement et attendre la fin (en particulier, lorsqu'une étape est traitée très rapidement) est entièrement justifié par l'OMI. Je dirais plutôt que Join est mauvais (en termes de FAQ C ++), ie. il ne doit être utilisé que si cela est vraiment nécessaire.
Spook
5
Je tiens à préciser clairement que Join est un outil, qui peut être utile, malgré le fait qu'il soit très souvent mal utilisé. Il existe certaines situations où cela fonctionnera sans effets secondaires inutiles (tels que le blocage du thread principal de l'interface graphique pendant un laps de temps notable).
Spook
2
Join est un outil? Je pense que vous trouverez que c'est une méthode.
Mitch Wheat
32

Les deux réponses précédentes sont excellentes et fonctionneront pour des scénarios simples. Il existe cependant d'autres moyens de synchroniser les threads. Ce qui suit fonctionnera également:

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 est l'un des différents WaitHandle que le framework .NET a à offrir. Ils peuvent fournir des capacités de synchronisation de threads beaucoup plus riches que les outils simples mais très courants comme lock () / Monitor, Thread.Join, etc. Ils peuvent également être utilisés pour synchroniser plus de deux threads, permettant des scénarios complexes tels qu'un thread `` maître '' qui coordonne plusieurs threads `` enfants '', plusieurs processus simultanés qui dépendent de plusieurs étapes les uns des autres à synchroniser, etc.

jrista
la source
30

Si vous utilisez à partir de .NET 4, cet exemple peut vous aider:

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
    }
}

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

Sayed Abolfazl Fatemi
la source
8
Vous n'avez rien mentionné à propos de Threads dans votre réponse. La question concerne les threads, pas les tâches. Les deux ne sont pas les mêmes.
Suamere
7
Je suis venu avec une question (et un niveau de connaissance) similaire à l'affiche originale et cette réponse a été très précieuse pour moi - les tâches sont bien plus adaptées à ce que je fais et si je n'avais pas trouvé cette réponse, j'aurais écrit mon propre pool de threads terrible.
Chris Rae
1
@ChrisRae, donc cela devrait être un commentaire dans la question d'origine, pas une réponse comme celle-ci.
Jaime Hablutzel
Comme il s'agit de cette réponse spécifique, je pense que cela a probablement plus de sens ici.
Chris Rae
1
comme @Suamere l'a dit, cette réponse n'a aucun lien avec la question OP.
Glenn Slayden
4

Je demanderais à votre thread principal de transmettre une méthode de rappel à votre premier thread, et quand c'est fait, il invoquera la méthode de rappel sur le thread principal, qui peut lancer le deuxième thread. Cela empêche votre fil principal de se bloquer pendant qu'il attend une jointure ou un Waithandle. Passer des méthodes en tant que délégués est une chose utile à apprendre de toute façon avec C #.

Ed Power
la source
0

Essaye ça:

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

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

foreach (Thread curThread in myThreads)
{
    curThread.Join();
}
Balises Wendell
la source
0

Publier pour peut-être aider d'autres personnes, j'ai passé pas mal de temps à chercher une solution comme celle que j'avais proposée. J'ai donc adopté une approche un peu différente. Il y a une option de compteur ci-dessus, je l'ai juste appliquée un peu différemment. J'étais en train de tourner de nombreux threads et j'ai incrémenté un compteur et décrémenté un compteur au fur et à mesure qu'un thread démarrait et s'arrêtait. Ensuite, dans la méthode principale, je voulais faire une pause et attendre la fin des threads.

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

Documenté sur mon blog. http://www.adamthings.com/post/2012/07/11/ensure-threads-have-finished-before-method-continues-in-c/

Adam
la source
4
C'est ce qu'on appelle l'attente occupée. Oui, cela fonctionne et c'est parfois la meilleure solution, mais vous voulez l'éviter si possible car cela gaspille du temps CPU
MobileMon
@MobileMon c'est plus une ligne directrice qu'une règle. Dans ce cas, étant donné que cette boucle peut gaspiller 0,00000001% du processeur et que l'OP code en C #, le remplacer par quelque chose de «plus efficace» serait une perte de temps totale. La première règle d'optimisation est - ne le faites pas. Mesurez d'abord.
Spike0xff
0

Lorsque je veux que l'interface utilisateur puisse mettre à jour son affichage en attendant la fin d'une tâche, j'utilise une boucle while qui teste IsAlive sur le thread:

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

Voici un exemple simple qui attend qu'une bande de roulement se termine, dans la même classe. Il fait également un appel à une autre classe dans le même espace de noms. J'ai inclus les instructions "using" afin qu'il puisse s'exécuter en tant que Winform tant que vous créez button1.

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}";
  }
 }
}
user3029478
la source