Закрытие боковой нити перед закрытием основной формы - PullRequest
4 голосов
/ 08 сентября 2011

Я делаю приложение с графическим интерфейсом для Windows-форм в Visual Studio .NET 2010, используя C#. У меня есть несколько потоков, например:

  1. Поток, который проверяет целевое устройство, чтобы убедиться, что соединение не потеряно
  2. Поток, который запрашивает и получает результаты измерений от устройства
  3. Поток, который постоянно обновляет основную форму с этими результатами. Этот поток вызывает изменения в основных элементах управления формы, используя Control.Invoke(). Моя цель: я хочу убедиться, что форма закрыта должным образом, когда пользователь нажимает (x) на главной форме. Это означает, что я хочу, чтобы мое соединение закрылось, все боковые потоки были разорваны и т. Д. Я пытаюсь использовать событие FormClosing. Там я установил для флага KillThreads значение true и ожидаю завершения побочных потоков. В каждом есть проверка, что-то вроде
if(KillThreads)
    return;

Но есть проблема. Если я попытаюсь дождаться завершения всех потоков

for(;;)
if(ActiveSideThreads == 0)
    break;
closeConnection();

поток N3 зависает, заблокированный вызовом Invoke(); Конечно, потому что основной поток графического интерфейса находится в бесконечном цикле. Таким образом, закрытие формы задерживается навсегда.

Если я реализую операции перед закрытием как процедуру потока 4 и создаю новый поток в обработчике событий FromClosing, форма закрывается сразу, до завершения потока 4, а Invoke() выдает ошибку - t. 4 не удалось закрыть поток 3 во времени. Если я добавлю Join(), Invoke() вызовы снова блокируются, и поток 3 никогда не прерывается, поэтому поток 4 ждет вечно.

Что мне делать? Есть ли способ решить эту проблему?

Ответы [ 4 ]

6 голосов
/ 08 сентября 2011

Прежде всего, решите, нужно ли вам вообще использовать Invoke. Как и раньше, мое мнение о том, что Invoke является одним из наиболее часто используемых методов, побуждающих к выполнению действия в потоке пользовательского интерфейса после его инициирования из рабочего потока. Альтернативой является создание некоторого типа объекта сообщения в рабочем потоке, описывающего действие, которое необходимо выполнить в потоке пользовательского интерфейса, и помещение его в общую очередь. Затем поток пользовательского интерфейса будет опрашивать эту очередь через некоторый интервал, используя System.Windows.Forms.Timer, и вызывать каждое действие. Это имеет несколько преимуществ.

  • Это нарушает тесную связь между пользовательским интерфейсом и рабочими потоками, которые навязывает Invoke.
  • Он возлагает ответственность за обновление потока пользовательского интерфейса на поток пользовательского интерфейса, к которому он все равно должен принадлежать.
  • Поток пользовательского интерфейса определяет, когда и как часто должно происходить обновление.
  • Нет риска переполнения насоса сообщений пользовательского интерфейса, как в случае с методами маршалинга, инициированными рабочим потоком.
  • Рабочему потоку не нужно ждать подтверждения того, что обновление было выполнено, прежде чем переходить к следующим шагам (т. Е. Вы получаете большую пропускную способность как для пользовательского интерфейса, так и для рабочих потоков).
  • На много проще обрабатывать операции выключения, поскольку вы можете практически полностью исключить все состояния гонки, которые обычно присутствуют при запросе на корректное завершение рабочих потоков.

Конечно, Invoke очень полезен, и есть много причин, чтобы продолжать использовать этот подход. Если вы решите продолжать использовать Invoke, тогда читайте дальше.

Одна из идей - установить флаги KillThreads в событии Form.Closing, а затем отменить закрытие формы, установив FormClosingEventArgs.Cancel = true. Возможно, вы захотите сообщить пользователю, что завершение работы было запрошено и выполняется. Вы можете отключить некоторые элементы управления в форме, чтобы новые действия не могли быть инициированы. Таким образом, в основном мы сигнализируем, что было запрошено завершение работы, но откладываем закрытие формы до тех пор, пока рабочие потоки не завершат сначала. Для этого вы можете запустить таймер, который периодически проверяет, закончились ли рабочие потоки, и если они есть, вы можете вызвать Form.Close.

public class YourForm : Form
{
  private Thread WorkerThread;
  private volatile bool KillThreads = false;

  private void YourForm_Closing(object sender, FormClosingEventArgs args)
  {
    // Do a fast check to see if the worker thread is still running.
    if (!WorkerThread.Join(0))
    {
      args.Cancel = true; // Cancel the shutdown of the form.
      KillThreads = true; // Signal worker thread that it should gracefully shutdown.
      var timer = new System.Timers.Timer();
      timer.AutoReset = false;
      timer.SynchronizingObject = this;
      timer.Interval = 1000;
      timer.Elapsed = 
        (sender, args) =>
        {
          // Do a fast check to see if the worker thread is still running.
          if (WorkerThread.Join(0)) 
          {
            // Reissue the form closing event.
            Close();
          }
          else
          {
            // Keep restarting the timer until the worker thread ends.
            timer.Start();
          }
        };
      timer.Start();
    }
  }    
}

Приведенный выше код вызывает Join, но он указывает время ожидания 0, которое вызывает немедленный возврат вызова Join. Это должно поддерживать прокачку сообщений пользовательского интерфейса.

1 голос
/ 08 сентября 2011

Я бы имел обработчик исключений верхнего уровня в вашем потоке, который выполняет Invoke, который глотает исключения, возникающие из Invoke, если обнаруживает, что KillThreads - это true. Тогда все, что вам нужно, это чтобы основной поток установил для KillThreads значение true и разрешил завершить цикл обработки сообщений.

Моя общая методика, а не простая логическая переменная, заключается в использовании ManualResetEvent в качестве сигнала для прерывания другими потоками. Основная причина для этого заключается в том, что у меня есть долго работающие потоки без пользовательского интерфейса, которые иногда хотят спать. Используя событие, я могу заменить вызовы на Thread.Sleep вызовами на Event.WaitOne, что означает, что поток всегда будет сразу же активироваться, если ему придется прервать работу.


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

0 голосов
/ 08 сентября 2011

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

0 голосов
/ 08 сентября 2011

Вы можете вызвать Thread.Join во всех потоках и убить через Thread.Abort, если они не могут быть соединены в течение заданного времени ожидания. Вызов Abort вызывает исключение ThreadAbortException в контексте потока, которое завершает этот поток.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...