Изящно закрыть поток переднего плана при остановке службы Windows

В моей службе Windows я создаю один «родительский» поток переднего плана, который, в свою очередь, порождает «дочерние» потоки, используя ThreadPool (что означает, что они являются фоновыми) для выполнения задач.

Каков наилучший способ изящно закрыть поток переднего плана при остановке службы Windows?

Вот моя текущая реализация (без привязки к конкретной задаче):

public partial class TaskScheduler : ServiceBase
{
   private static AutoResetEvent _finishedTaskAutoResetEvent = new AutoResetEvent(false);

   //This flag is used to increase chances of the Spawning Thread to finish gracefully when service stops.
   private bool StopRequested { get; set; }

   private int _executingTasksCount;

   private int ExecutingTasksCount { get { return _executingTasksCount; } }

   private void IncCurrentTasksCount()
   {
       Interlocked.Increment(ref _executingTasksCount);
   }

   private void DecCurrentTasksCount()
   {
       Interlocked.Decrement(ref _executingTasksCount);
   }

   public TaskScheduler()
   {
       InitializeComponent();

       Thread spawningThread = new Thread(DoSpawnTaskExecutionThreads);

       spawningThread.Name = "Spawning Thread";
       spawningThread.IsBackground = false;
       spawningThread.Start();
   }

   protected override void OnStart(string[] args)
   {
   }

   protected override void OnStop()
   {
       StopRequested = true;
   }

   private void DoSpawnTaskExecutionThreads()
   {
       //We check StopRequested to try and finish this thread gracefully when service stops.
       while (!StopRequested)
       {
           while (!StopRequested && ExecutingTasksCount < MaxPooledTasks)
           {
               ThreadPool.QueueUserWorkItem(ExecuteTask, new Task());

               IncCurrentTasksCount();
           }

           _finishedTaskAutoResetEvent.WaitOne();
       }

       //Either all task execution threads will finish or the process will be terminated forcibly.
       while (ExecutingTasksCount > 0)
       {
           Thread.Sleep(200); //Check five times a second.
       }

       _eventLog.WriteEntry("The Spawning Thread finished along with task execution threads.");
   }

   private void ExecuteTask(object state)
   {
       try
       {
           Task task = (Task)state;

           task.Execute();
       }
       catch
       {
           // Handle exception.
       }
       finally
       {
           DecCurrentTasksCount();
           _finishedTaskAutoResetEvent.Set();
       }
   }

}


person Den    schedule 25.10.2010    source источник


Ответы (3)


Я вижу пару проблем с кодом.

  • Проверка StopRequested не является поточно-ориентированной.
  • Проверка ExecutingTaskCount не является поточно-ориентированной.
  • Поскольку _finishedTaskAutoResetEvent является AutoResetEvent, сигналы могут быть потеряны, потому что WaitHandle не поддерживает счет. Может быть, это то, что вы хотите, но это может привести к странному вращению вложенных while циклов.

Вот как я бы реорганизовал ваш код. Он использует класс CountdownEvent, доступный в .NET 4.0.

public class TaskScheduler : ServiceBase
{
    private m_Stop as ManualResetEvent = new ManualResetEvent(false);

    protected override void OnStart(string[] args)           
    {           
      var thread = new Thread(DoSpawnTaskExecutionThreads);
      thread.Name = "Spawning Thread";
      thread.IsBackground = false;
      thread.Start();
    }           

    protected override OnStop()
    {
      m_Stop.Set();
    }

    public DoSpawnTaskExecutionThreads()
    {
      // The semaphore will control how many concurrent tasks can run.
      var pool = new Semaphore(MaxPooledThreads, MaxPooledThreads);

      // The countdown event will be used to wait for any pending tasks.
      // Initialize the count to 1 so that we treat this thread as if it 
      // were a work item. This is necessary to avoid a subtle race
      // with a real work item that completes quickly.
      var tasks = new CountdownEvent(1);

      // This array will be used to control the spinning of the loop.
      var all = new WaitHandle[] { pool, m_Stop };

      while (WaitHandle.WaitAny(all) == 0)
      {
        // Indicate that there is another task.
        tasks.AddCount();

        // Queue the task.
        Thread.QueueUserWorkItem(
          (state) =>
          {
            try
            {
              var task = (Task)state;
              task.Execute();
            }
            finally
            {
              pool.Release(); // Allow another task to be queued.
              tasks.Signal(); // Indicate that this task is complete.
            }
          }, new Task());
      }

      // Indicate that the main thread is complete.
      tasks.Signal();

      // Wait for all pending tasks.
      tasks.Wait();
    }
}
person Brian Gideon    schedule 25.10.2010
comment
Спасибо за подробное объяснение и образец кода. Однако у меня есть несколько вопросов: 1) Проверка ExecutingTaskCount не является поточно-ориентированной: почему? Я только модифицирую его с помощью класса Interlocked. Если бы я все еще хотел использовать его по какой-то причине, как бы я это сделал? Когда вы бы порекомендовали использовать класс Interlocked? 2) ... _finishedTaskAutoResetEvent - это AutoResetEvent, сигналы могут быть потеряны, потому что WaitHandle не поддерживает счетчик ...: Что может быть причиной такой ситуации? Задача выдает необработанное исключение, а я по какой-то причине не обрабатываю его? - person Den; 26.10.2010
comment
RE # 1 ... Чтобы сделать ExecutingTasksCount потокобезопасным, вам нужно будет выполнить непостоянное чтение _executingTasksCount. Это можно сделать с помощью метода Interlocked.CompareExchange или пометив переменную как volatile. - person Brian Gideon; 26.10.2010
comment
RE # 2 ... Представьте себе гипотетический сценарий (который очень маловероятен), когда все потоки задач будут вытеснены между DecCurrentTasksCount и _finishedTaskAutoResetEvent.Set. Я думаю, что ваши вложенные циклы защищают от любых проблем, но я предвижу странные способы их поведения. Опять же, я не думаю, что в этом подходе есть какие-то проблемы, но об этом трудно думать. - person Brian Gideon; 26.10.2010

Я вижу здесь одну проблему:

StopRequested не должен быть автоматическим свойством. Вы должны определить это как свойство с резервным полем, чтобы пометить его изменчивый.

private volatile bool stopRequested;
private bool StopRequested
{
    get { return this.stopRequested; }
    set { this.stopRequested = value; }
}

Без этого возможно, что условие выхода не будет замечено (по крайней мере, сразу) вашим потоком, когда оно установлено службой.

Кроме того, если .NET 4 является вариантом, существует гораздо более простой вариант, который можно реализовать с помощью CancellationToken и BlockingCollection<T>.

person Reed Copsey    schedule 25.10.2010
comment
Единственное место, где я собираюсь изменить StopRequested, - это OnStop (). Спасибо за предложение. - person Den; 25.10.2010
comment
@Den: OnStop будет вызываться из отдельного потока. Без этого поток TaskScheduler (обязательно) не увидит изменения. - person Reed Copsey; 25.10.2010
comment
@Den: По сути, произойдет то, что JIT может увидеть, что DoSpawnTaskExecutionThreads никогда не изменится StopRequested, и поэтому он может попытаться оптимизировать повторяющиеся чтения, подняв их и объединив в одно за пределами цикла и над циклом. Вызов WaitOne в цикле остановит компилятор от выполнения этой оптимизации на самом деле, но в этом случае он сработает случайно. Здесь лучше всего следовать совету Рида, чтобы не было никаких сомнений. - person Brian Gideon; 26.10.2010

Вы можете использовать метод Join для «изящного» завершения потока. MSDN содержит некоторую информацию о методе.

person Pedro    schedule 25.10.2010
comment
Здесь это не сработает - вы не можете заблокировать поток службы, или хост службы принудительно уничтожит его, так как существует тайм-аут на закрытие служб. - person Reed Copsey; 25.10.2010