Как ждать окончания потока с .NET?


178

Я никогда раньше не использовал потоки в C #, где мне нужно иметь два потока, а также основной поток пользовательского интерфейса. В основном у меня есть следующее.

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

Итак, по сути, мой вопрос заключается в том, как заставить поток ждать завершения другого. Каков наилучший способ сделать это?


4
Если вы «... никогда раньше не использовали многопоточность», вы можете рассмотреть некоторые более простые альтернативы, такие как шаблон * Async ().
Ðаn

4
Если вы все еще ожидаете завершения потока 1, почему вы не просто вызываете этот метод синхронно?
Свиш

13
Какой смысл в использовании потоков при линейной обработке?
Джон

1
@ Джон, для меня совершенно логично, что есть много способов использовать фоновый поток, который работает, пока работает пользователь. Кроме того, ваш вопрос не такой же, как предыдущий?
user34660

Ответ Rotem , используя backgroundworker для простоты использования, очень прост.
Камил

Ответы:


264

Я вижу 5 доступных вариантов:

1. Thread.Join

Как с ответом Митча. Но это заблокирует ваш поток пользовательского интерфейса, однако вы получите встроенный тайм-аут.


2. Используйте WaitHandle

ManualResetEvent это WaitHandle как предложила Джриста.

Стоит отметить, что если вы хотите дождаться нескольких потоков, WaitHandle.WaitAll()по умолчанию они не будут работать, так как для этого требуется поток MTA. Вы можете обойти это, пометив свой Main()метод сMTAThread - однако это блокирует вашу рассылку сообщений и не рекомендуется из того, что я прочитал.


3. Запустить событие

Посмотрите эту страницу Джона Скита о событиях и многопоточности, возможно, что событие может стать неподписанным между ... ifи EventName(this,EventArgs.Empty)это случилось со мной раньше.

(Надеюсь, эти компиляции я не пробовал)

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. Используйте делегата

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

Если вы используете метод _count, возможно, было бы (если быть безопасным) увеличить его, используя

Interlocked.Increment(ref _count)

Мне было бы интересно узнать разницу между использованием делегатов и событий для уведомления о потоках, единственное различие, которое я знаю, это то, что события вызываются синхронно.


5. Вместо этого делайте это асинхронно

Ответ на этот вопрос имеет очень четкое описание ваших вариантов с помощью этого метода.


Делегат / События в неправильной теме

Способ выполнения события / делегата будет означать, что ваш метод обработчика событий находится в thread1 / thread2, а не в основном потоке пользовательского интерфейса , поэтому вам нужно переключиться обратно вверху методов HandleThreadDone:

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

61

Добавить

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

после того, как вы запустите его, но это не даст большого результата, поскольку это по сути тот же результат, что и запуск в основном потоке!

Я настоятельно рекомендую прочитать электронную книгу Джо Албахари по созданию потоков в C # , если вы хотите получить представление о потоках в .NET.


4
Несмотря на то, Joinчто буквально то, о чем спрашивает спрашивающий, в целом это может быть очень плохо. Вызов to Joinповесит поток, из которого это делается. Если это основной поток GUI, это ПЛОХО ! Как пользователь я активно ненавижу приложения, которые, кажется, работают таким образом. Поэтому, пожалуйста, смотрите все остальные ответы на этот вопрос и stackoverflow.com/questions/1221374/…
peSHIr

1
Я согласен, что вообще Join () это плохо. Возможно, я не сделал это достаточно очевидным в своем ответе.
Митч Уит

2
Ребята, один размер подходит не всем . Существуют ситуации, когда действительно необходимо убедиться, что поток завершил свою работу: учтите, что поток обрабатывает данные, которые вот-вот будут изменены. В таком случае оповещение потока об отмене изящно и ожидание его завершения (особенно, когда один шаг обрабатывается очень быстро) полностью оправдано. Я бы скорее сказал, что Join - это зло (в терминах C ++ FAQ), т.е. он не должен использоваться, если это действительно не требуется.
Spook

5
Я хочу четко заявить, что Join - это инструмент, который может быть полезен, несмотря на тот факт, что им очень часто злоупотребляют. Бывают ситуации, когда он будет работать без каких-либо ненужных побочных эффектов (например, остановка основного потока графического интерфейса на заметное время).
Призрак

2
Присоединиться это инструмент? Я думаю, вы найдете это метод.
Митч Пшеничный

32

Предыдущие два ответа великолепны и будут работать для простых сценариев. Однако есть и другие способы синхронизации потоков. Следующее также будет работать:

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 является одним из различных WaitHandle , которые может предложить .NET Framework. Они могут обеспечить гораздо более широкие возможности синхронизации потоков, чем простые, но очень распространенные инструменты, такие как lock () / Monitor, Thread.Join и т. Д. Они также могут использоваться для синхронизации более двух потоков, что позволяет создавать сложные сценарии, такие как «главный» поток. который координирует несколько «дочерних» потоков, несколько параллельных процессов, которые зависят от нескольких этапов друг друга для синхронизации, и т. д.


30

При использовании из .NET 4 этот пример может помочь вам:

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

от: https://stackoverflow.com/a/4190969/1676736


8
Вы ничего не упомянули о темах в своем ответе. Вопрос о нитях, а не о задачах. Два не одинаковы.
Суамер

7
Я пришел с похожим вопросом (и уровнем знаний) к оригинальному постеру, и этот ответ был очень ценным для меня - задачи более подходят для того, что я делаю, и если бы я не нашел этот ответ, я бы написал свой собственный ужасный пул потоков.
Крис Рей

1
@ChrisRae, так что это должен быть комментарий в исходном вопросе, а не ответ, подобный этому.
Хайме

Поскольку речь идет об этом конкретном ответе, я думаю, что здесь, вероятно, больше смысла.
Крис Рей

1
как сказал @Suamere, этот ответ совершенно не связан с вопросом ОП.
Гленн


4

Я хотел бы, чтобы ваш основной поток передавал метод обратного вызова в ваш первый поток, и когда это будет сделано, он вызовет метод обратного вызова в основной цепочке, который может запустить второй поток. Это удерживает ваш основной поток от зависания во время ожидания соединения или ожидания. Передача методов в качестве делегатов в любом случае полезна для изучения на C #.


0

Попробуй это:

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

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

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

0

Публикуясь, чтобы помочь некоторым другим, потратил довольно много времени в поисках решения, подобного тому, что я придумал. Поэтому я выбрал немного другой подход. Выше есть счетчик, я применил его немного по-другому. Я раскручивал множество потоков, увеличивал счетчик и уменьшал счетчик, когда поток начинался и останавливался. Затем в основном методе я хотел приостановить и дождаться завершения потоков, что я и сделал.

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

Задокументировано в моем блоге. http://www.adamthings.com/post/2012/07/11/ensure-threads-have-finished-before-method-continues-in-c/


4
Это называется занятым ожиданием. Да, это работает, а иногда и является лучшим решением, но вы хотите избежать его, если это возможно, потому что это тратит процессорное время
MobileMon

@MobileMon - это скорее руководство, чем правило. В этом случае, поскольку этот цикл может тратить 0,00000001% ЦП, а OP кодирует в C #, замена его на что-то «более эффективное» будет полной тратой времени. Первое правило оптимизации - нет. Измерьте в первую очередь.
Spike0xff

0

Когда я хочу, чтобы пользовательский интерфейс мог обновлять свой дисплей в ожидании завершения задачи, я использую цикл while, который проверяет IsAlive в потоке:

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

-1

Вот простой пример, который ожидает завершения шага в том же классе. Он также выполняет вызов другого класса в том же пространстве имен. Я включил операторы using, чтобы он мог выполняться как Winform, пока вы создаете 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}";
  }
 }
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.