Як чекати, коли нитка закінчиться .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
@John, для мене цілком має сенс, що існує багато застосувань для відкручування фонової нитки, яка працює, поки користувач працює. Крім того, ваше запитання не збігається з попереднім?
користувач34660

Відповідь Ротема , використовуючи фонового робота для легкого використання, дуже проста.
Каміль

Відповіді:


264

Я бачу доступні 5 варіантів:

1. Нитка

Як і у відповіді Мітча. Але це заблокує ваш потік користувальницького інтерфейсу, проте ви отримаєте вбудований для вас тайм-аут.


2. Використовуйте a WaitHandle

ManualResetEventє, WaitHandleяк запропонував jrista.

Варто зазначити, що якщо ви хочете зачекати кілька потоків, 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, що запитувач, як видається, просить буквально, це, як правило, може бути дуже погано. Заклик до Joinвішає тему, з якої це робиться. Якщо це є головним потоком GUI, це BAD ! Як користувач я активно ненавиджу програми, які, здається, працюють таким чином. Так дивіться всі інші відповіді на це питання і stackoverflow.com/questions/1221374 / ...
peSHIr

1
Я згоден, що загалом Join () поганий. Я, можливо, не зробив цього досить очевидним у своїй відповіді.
Мітч Пшеничний

2
Хлопці, один розмір підходить не всім . Бувають ситуації, коли справді потрібно бути впевненим, що нитка закінчила свою роботу: врахуйте, що потік обробляє дані, які тільки збираються змінити. У такому випадку сповіщення потоку відмінятись граціозно та чекати, поки він закінчиться (особливо, коли один крок обробляється дуже швидко) IMO повністю виправданий. Я скоріше скажу, що Join є злом (в C ++ FAQ FAQ), тобто. він не повинен використовуватися, якщо реально цього не потрібно.
Spook

5
Я хочу чітко заявити, що Join - це інструмент, який може бути корисним, незважаючи на те, що його дуже часто зловживають. Існують деякі ситуації, коли це просто спрацює без зайвих побічних ефектів (наприклад, затримка основного потоку GUI протягом помітного часу).
Spook

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
Ви нічого не згадали про нитки у своїй відповіді. Питання стосується теми, а не завдань. Два не однакові.
Suamere

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

1
@ChrisRae, тож це має бути коментар до оригінального питання, а не відповідь на зразок цього.
Хайме Хаблуцель

Оскільки мова йде про цю конкретну відповідь, я думаю, що це, мабуть, має більше сенсу.
Кріс Рай

1
як сказав @Suamere, ця відповідь повністю не пов'язана з питанням про ОП.
Гленн Слейден


4

Я б змусив ваш основний потік передати метод зворотного виклику до вашого першого потоку, і коли це буде виконано, він викличе метод зворотного виклику на mainthadad, який може запустити другий потік. Це запобігає зависанню вашої основної нитки під час очікування приєднання або чекання. Передача методів як делегатів - корисна річ, з якою все-таки навчитися 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% процесора, а ОП кодується в C #, замінити це чимось "більш ефективним" було б повною тратою часу. Перше правило оптимізації - не робити. Виміряйте спочатку.
Spike0xff

0

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

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

-1

Ось простий приклад, який чекає закінчення протектора в межах одного класу. Він також робить дзвінок до іншого класу в тому ж просторі імен. Я включив оператори "using", щоб він міг виконуватись як Winform, поки ви створюєте кнопку1.

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.