Jak czekać na zakończenie wątku z .NET?


178

Nigdy wcześniej nie korzystałem z wątków w C #, w których muszę mieć dwa wątki, a także główny wątek interfejsu użytkownika. Zasadniczo mam następujące.

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

Zasadniczo moje pytanie brzmi: jak sprawić, by wątek czekał na zakończenie innego. Jaki jest najlepszy sposób, aby to zrobić?


4
Jeśli „… nigdy wcześniej nie korzystałeś z wątków…”, możesz rozważyć kilka prostszych alternatyw, takich jak wzorzec * Async ().
Ðаn

4
Jeśli i tak czekasz tylko na zakończenie wątku 1, dlaczego nie wywołujesz tej metody po prostu synchronicznie?
Svish

13
Jaki jest sens używania wątków podczas przetwarzania w sposób liniowy?
John

1
@John, to ma dla mnie absolutny sens, że istnieje wiele zastosowań do wyodrębnienia wątku w tle, który działa podczas pracy użytkownika. Poza tym, czy twoje pytanie nie jest takie samo jak poprzednie?
user34660

Odpowiedź Rotem , używając narzędzia do pracy w tle, jest bardzo prosta.
Kamil

Odpowiedzi:


264

Widzę 5 dostępnych opcji:

1. Thread.Join

Jak w przypadku odpowiedzi Mitcha. Ale to zablokuje twój wątek interfejsu użytkownika, jednak otrzymasz wbudowany limit czasu.


2. Użyj WaitHandle

ManualResetEventjest WaitHandlejak sugerowała jrista.

Należy zwrócić uwagę na to, że jeśli chcesz czekać na wiele wątków, WaitHandle.WaitAll()nie będzie działać domyślnie, ponieważ wymaga wątku MTA. Możesz to obejść, oznaczając swoją Main()metodę znakiem MTAThread- jednak blokuje to twoją pompę wiadomości i nie jest zalecane z tego, co przeczytałem.


3. Odpal wydarzenie

Zobacz tę stronę Jon Skeet o wydarzeniach i wielowątkowości, to możliwe, że zdarzenie może stać unsubcribed pomiędzy ifiEventName(this,EventArgs.Empty) - zdarzyło mi się to wcześniej.

(Mam nadzieję, że te kompilacje, nie próbowałem)

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. Użyj pełnomocnika

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

Jeśli używasz metody _count, może być pomysłem (ze względów bezpieczeństwa), aby ją zwiększyć za pomocą

Interlocked.Increment(ref _count)

Chciałbym poznać różnicę między używaniem delegatów a zdarzeniami do powiadamiania o wątkach, jedyną różnicą, jaką znam, jest to, że zdarzenia są wywoływane synchronicznie.


5. Zrób to asynchronicznie

Odpowiedź na to pytanie zawiera bardzo jasny opis możliwości tej metody.


Deleguj / Zdarzenia w niewłaściwym wątku

Sposób działania zdarzenia / delegata będzie oznaczał, że metoda obsługi zdarzeń znajduje się w wątku1 / wątku2, a nie w głównym wątku interfejsu użytkownika , więc będziesz musiał przełączyć się z powrotem na górę metod HandleThreadDone:

// Delegate example
if (InvokeRequired)
{
    Invoke(new Action(HandleThreadDone));
    return;
}

61

Dodaj

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

po uruchomieniu, ale to nie da wiele, ponieważ jest to zasadniczo taki sam wynik, jak uruchomienie w głównym wątku!

Mogę gorąco polecić przeczytanie darmowego e-booka Joe Albahari's Threading in C # , jeśli chcesz poznać wątki w .NET.


4
Chociaż Joinwydaje się, że pytający dosłownie o to prosił, może to być ogólnie bardzo złe. Wezwanie do Joinrozłączy wątek, z którego to się dzieje. Jeśli to jest główny wątek GUI, to jest ZŁE ! Jako użytkownik aktywnie nienawidzę aplikacji, które wydają się działać w ten sposób. Zobacz więc wszystkie inne odpowiedzi na to pytanie i stackoverflow.com/questions/1221374/ ...
peSHIr,

1
Zgadzam się, że ogólnie Join () jest zły. Być może nie wyraziłem tego wystarczająco jasno w mojej odpowiedzi.
Mitch Wheat,

2
Chłopaki, jeden rozmiar nie pasuje do wszystkich . Są sytuacje, kiedy naprawdę trzeba mieć pewność, że wątek zakończył swoją pracę: weźmy pod uwagę, że wątek przetwarza dane, które dopiero mają zostać zmienione. W takim przypadku wdzięczne powiadomienie wątku o anulowaniu i oczekiwanie na jego zakończenie (szczególnie, gdy jeden krok jest przetwarzany bardzo szybko) jest w pełni uzasadnione przez IMO. Powiedziałbym raczej, że Join jest zły (w kategoriach C ++ FAQ), tj. nie powinno być używane, chyba że jest to naprawdę wymagane.
Spook

5
Chcę jasno stwierdzić, że Join to narzędzie, które może się przydać, mimo że bardzo często jest nadużywane. Istnieją sytuacje, w których będzie działać bez żadnych niepotrzebnych efektów ubocznych (takich jak opóźnienie głównego wątku GUI na zauważalny czas).
Spook

2
Dołącz to narzędzie? Myślę, że przekonasz się, że to metoda.
Mitch Wheat

32

Poprzednie dwie odpowiedzi są świetne i sprawdzą się w prostych scenariuszach. Istnieją jednak inne sposoby synchronizowania wątków. Poniższe również będą działać:

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 jest jednym z wielu WaitHandle , które ma do zaoferowania platforma .NET. Mogą zapewnić znacznie bogatsze możliwości synchronizacji wątków niż proste, ale bardzo popularne narzędzia, takie jak lock () / Monitor, Thread.Join itp. Mogą być również używane do synchronizowania więcej niż dwóch wątków, umożliwiając złożone scenariusze, takie jak wątek „główny” który koordynuje wiele wątków `` potomnych '', wiele współbieżnych procesów, które są zależne od kilku etapów wzajemnej synchronizacji itp.


30

W przypadku korzystania z platformy .NET 4 ten przykład może pomóc:

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

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


8
W swojej odpowiedzi nie wspomniałeś o wątkach. Pytanie dotyczy wątków, a nie zadań. Nie są takie same.
Suamere

7
Przyszedłem z podobnym pytaniem (i poziomem wiedzy) do oryginalnego plakatu i ta odpowiedź była dla mnie bardzo cenna - zadania są o wiele bardziej odpowiednie do tego, co robię i gdybym nie znalazł tej odpowiedzi, napisałbym własny okropny basen nici.
Chris Rae

1
@ChrisRae, więc powinien to być komentarz do pierwotnego pytania, a nie odpowiedź jak ta.
Jaime Hablutzel

Ponieważ chodzi o tę konkretną odpowiedź, myślę, że prawdopodobnie ma ona tutaj większy sens.
Chris Rae

1
jak powiedział @Suamere, ta odpowiedź jest całkowicie niezwiązana z pytaniem PO.
Glenn Slayden


4

Chciałbym, aby twój główny wątek przekazał metodę wywołania zwrotnego do twojego pierwszego wątku, a kiedy to się skończy, wywoła metodę wywołania zwrotnego na głównym wątku, który może uruchomić drugi wątek. Dzięki temu główny wątek nie zwisa podczas oczekiwania na połączenie lub Waithandle. Przekazywanie metod jako delegatów jest i tak przydatna do nauczenia się w języku C #.


0

Spróbuj tego:

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

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

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

0

Wysyłając, aby być może pomóc innym, spędziłem trochę czasu na poszukiwaniu rozwiązania takiego jak to, co wymyśliłem. Więc przyjąłem trochę inne podejście. Powyżej znajduje się opcja licznika, po prostu zastosowałem ją nieco inaczej. Odpędzałem wiele wątków, zwiększałem licznik i zmniejszałem licznik, gdy wątek zaczynał się i kończył. Następnie w głównej metodzie chciałem wstrzymać i poczekać na zakończenie wątków.

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

Udokumentowane na moim blogu. http://www.adamthings.com/post/2012/07/11/ensure-threads-have-finished-before-method-continues-in-c/


4
Nazywa się to czekaniem zajęty. Tak, działa i czasami jest najlepszym rozwiązaniem, ale chcesz tego uniknąć, jeśli to możliwe, ponieważ marnuje czas procesora
MobileMon,

@MobileMon to bardziej wskazówka niż reguła. W tym przypadku, ponieważ pętla ta może marnować 0,00000001% procesora, a OP koduje w C #, zastąpienie tego czymś „bardziej wydajnym” byłoby całkowitą stratą czasu. Pierwsza zasada optymalizacji brzmi - nie rób tego. Najpierw zmierz.
Spike0xff

0

Kiedy chcę, aby interfejs użytkownika mógł aktualizować swój wyświetlacz podczas oczekiwania na zakończenie zadania, używam pętli while, która testuje IsAlive w wątku:

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

-1

Oto prosty przykład, który czeka na zakończenie bieżnika w tej samej klasie. Wykonuje również wywołanie innej klasy w tej samej przestrzeni nazw. Dołączyłem instrukcje „using”, aby mógł być wykonywany jako Winform tak długo, jak długo tworzysz 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}";
  }
 }
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.