2016-04-06 3 views
3

У меня есть класс, который запускает задачу и хочет, чтобы задача прекращалась, когда объект был собран мусором.Отмена задачи при завершении объекта

Я реализовал шаблон IDisposable, чтобы гарантировать, что если объект размещен вручную или используется в используемом блоке, задача останавливается правильно. Однако, я не могу гарантировать, что конечный пользователь вызовет Dispose() или использует объект в блоке использования. Я знаю, что сборщик мусора в конечном итоге назовет Finalizer - значит ли это, что задача оставлена?

public class MyClass : IDisposable 
{ 
    private readonly CancellationTokenSource feedCancellationTokenSource = 
      new CancellationTokenSource(); 

    private readonly Task feedTask; 

    public MyClass() 
    { 
     feedTask = Task.Factory.StartNew(() => 
     { 
      while (!feedCancellationTokenSource.IsCancellationRequested) 
      { 
       // do finite work 
      } 
     }); 
    } 

    public void Dispose() 
    { 
     Dispose(true); 
     GC.SuppressFinalize(this); 
    } 

    protected virtual void Dispose(bool disposing) 
    { 
     if (disposing) 
     { 
      feedCancellationTokenSource.Cancel(); 
      feedTask.Wait(); 

      feedCancellationTokenSource.Dispose(); 
      feedTask.Dispose(); 
     } 
    } 

    ~MyClass() 
    { 
     Dispose(false); 
    } 
} 

Был предложен в this question добавить летучее логическое значение, которое устанавливается с Finalizer и наблюдаемое от задачи. Это рекомендуется, или есть лучший способ добиться того, что мне нужно?

(я использую .NET 4, следовательно, использование TaskFactory.StartNew, а не Task.Run)

EDIT:

Чтобы дать некоторый контекст на вопрос - который на самом деле не показанным в приведенном выше фрагменте кода: Я создаю класс сетевого клиента, у которого есть механизм для поддержания регулярности отправки пакетов на сервер. Я решил не приводить все эти детали в примере, поскольку это не имело никакого отношения к моему конкретному вопросу. Однако то, что я действительно хочу, - это возможность для пользователя установить для свойства KeepAlive boolean значение true, которое запустит задачу отправки данных на сервер каждые 60 секунд. Если пользователь устанавливает свойство в false, задача останавливается. IDisposable получил мне 90% пути, однако он полагается на то, что пользователь правильно распоряжается им (явно или с помощью). Я не хочу раскрывать для пользователя задачи сохранения, чтобы они явно меняли, я просто хочу, чтобы «простой» KeepAlive = true/false запускал/останавливал задачу. И я хочу, чтобы задача остановилась, когда пользователь закончил с объект - даже если они не распоряжаются им должным образом. Я начинаю думать, что это невозможно!

+0

Почему бы не использовать 'System.Timers.Timer'? –

+2

Этот код ничего не сделает в финализаторе. Это ошибка. Кроме того, поскольку задача удерживается на внешнем экземпляре MyClass через его состояние закрытия, этот объект никогда не будет завершен до тех пор, пока задача не завершится естественным образом. – usr

+0

@ Danny Chen Я считал это - но имеет ли та же проблема? Насколько я понимаю, таймер будет продолжать работать, даже если объект, создавший его, будет удален (пожалуйста, поправьте меня, если я ошибаюсь)! Я подумал о том, чтобы установить AutoReset в false и сделать метод обратного вызова вручную сбросить его каждый раз, когда он «тикает», - но тогда не было уверенности, что GC когда-либо завершит объект, если он подписался на событие. Мы ценим любые предложения! –

ответ

2

Нарисую ответ. Я не уверен на 100%, что это сработает. Завершение является сложной проблемой, и я не владею ею.

  1. Не может быть ссылки на объект из задачи на любой объект, который должен быть завершен.
  2. Вы не можете трогать другие объекты из финализатора, которые, как известно, не безопасны. Встроенные классы .NET обычно не документируют это свойство безопасности. Вы не можете полагаться на это (обычно).
class CancellationFlag { public volatile bool IsSet; } 

Теперь вы можете поделиться экземпляр этого класса между задачей и MyClass. Задача должна опросить флаг, и MyClass должен установить его.

Для того, чтобы убедиться, что задача никогда не случайно ссылается на внешний объект, я бы структурировать код так:

Task.Factory.StartNew(TaskProc, state); //no lambda 

static void TaskProc(object state) { //static 
} 

Таким образом, вы можете явно распараллеливания любое состояние через state. Это, по крайней мере, было бы примером CancellationFlag, но ни при каких обстоятельствах не ссылалось на MyClass.

+0

Я бы сделал это аналогично, за исключением того, что я поместил бы volatile bool флаг непосредственно в потребительский класс, так как это устранило бы любую потребность в рассмотрении вопроса о том, безопасно ли «CancellationFlag» или нет (хотя в этом случае это тривиально) , – Douglas

+0

Что вы подразумеваете под потребляющим классом? Он не может помещать его в MyClass, потому что это предотвращает завершение. Если вы имеете в виду класс, который он, вероятно, должен создать для 'state', тогда я согласен. – usr

+0

Вы правы; Я снова пропустил закрытие. Я думаю, что это лучший способ сделать это. – Douglas

1

Я создал программу ниже, чтобы изучить различия ...

Из моих наблюдений с ним, похоже, не имеет значения, является ли это токеном отмены или изменчивым bool, что действительно важно, так как метод Task.StartNew не вызывается с использованием выражения лямбда.

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

Пожалуйста, попробуйте и дайте мне знать, если вы пришли к такому же выводу.

using System; 
using System.Collections.Generic; 
using System.IO; 
using System.Linq; 
using System.Text; 
using System.Threading; 
using System.Threading.Tasks; 

namespace ConsoleApplication7 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      Logger.LogFile = @"c:\temp\test\log.txt"; 

      Task.Run(() => 
      { 
       // two instances (not disposed properly) 

       // if left to run, this background task keeps running until the application exits 
       var c1 = new MyClassWithVolatileBoolCancellationFlag(); 

       // if left to run, this background task cancels correctly 
       var c2 = new MyClassWithCancellationSourceAndNoLambda(); 

       // 
       var c3 = new MyClassWithCancellationSourceAndUsingTaskDotRun(); 

       // 
       var c4 = new MyClassWithCancellationSourceAndUsingTaskDotRunButNoParentReference(); 


      }).GetAwaiter().GetResult(); 

      // instances no longer referenced at this point 

      Logger.Log("Press Enter to exit"); 
      Console.ReadLine(); // press enter to allow the console app to exit normally: finalizer gets called on both instances 
     } 


     static class Logger 
     { 
      private static object LogLock = new object(); 
      public static string LogFile; 
      public static void Log(string toLog) 
      { 
       try 
       { 
        lock (LogLock) 
         using (var f = File.AppendText(LogFile)) 
          f.WriteLine(toLog); 

        Console.WriteLine(toLog); 
       } 
       catch (Exception ex) 
       { 
        Console.WriteLine("Logging Exception: " + ex.ToString()); 
       } 
      } 

     } 

     // finalizer gets called eventually (unless parent process is terminated) 
     public class MyClassWithCancellationSourceAndUsingTaskDotRunButNoParentReference : IDisposable 
     { 
      private CancellationTokenSource cts = new CancellationTokenSource(); 

      private readonly Task feedTask; 

      public MyClassWithCancellationSourceAndUsingTaskDotRunButNoParentReference() 
      { 
       Logger.Log("New MyClassWithCancellationSourceAndUsingTaskDotRunButNoParentReference Instance"); 

       var token = cts.Token; // NB: by extracting the struct here (instead of in the lambda in the next line), we avoid the parent reference (via the cts member variable) 
       feedTask = Task.Run(() => Background(token)); // token is a struct 
      } 

      private static void Background(CancellationToken token) // must be static or else a reference to the parent class is passed 
      { 
       int i = 0; 
       while (!token.IsCancellationRequested) // reference to cts means this class never gets finalized 
       { 
        Logger.Log("Background task for MyClassWithCancellationSourceAndUsingTaskDotRunButNoParentReference running. " + i++); 
        Thread.Sleep(1000); 
       } 
      } 

      public void Dispose() 
      { 
       Dispose(true); 
       GC.SuppressFinalize(this); 
      } 

      protected virtual void Dispose(bool disposing) 
      { 
       cts.Cancel(); 

       if (disposing) 
       { 
        feedTask.Wait(); 

        feedTask.Dispose(); 

        Logger.Log("MyClassWithCancellationSourceAndUsingTaskDotRunButNoParentReference Disposed"); 
       } 
       else 
       { 
        Logger.Log("MyClassWithCancellationSourceAndUsingTaskDotRunButNoParentReference Finalized"); 
       } 
      } 

      ~MyClassWithCancellationSourceAndUsingTaskDotRunButNoParentReference() 
      { 
       Dispose(false); 
      } 
     } 

     // finalizer doesn't get called until the app is exiting: background process keeps running 
     public class MyClassWithCancellationSourceAndUsingTaskDotRun : IDisposable 
     { 
      private CancellationTokenSource cts = new CancellationTokenSource(); 

      private readonly Task feedTask; 

      public MyClassWithCancellationSourceAndUsingTaskDotRun() 
      { 
       Logger.Log("New MyClassWithCancellationSourceAndUsingTaskDotRun Instance"); 
       //feedTask = Task.Factory.StartNew(Background, cts.Token); 
       feedTask = Task.Run(() => Background()); 
      } 

      private void Background() 
      { 
        int i = 0; 
        while (!cts.IsCancellationRequested) // reference to cts & not being static means this class never gets finalized 
        { 
         Logger.Log("Background task for MyClassWithCancellationSourceAndUsingTaskDotRun running. " + i++); 
         Thread.Sleep(1000); 
        } 
      } 

      public void Dispose() 
      { 
       Dispose(true); 
       GC.SuppressFinalize(this); 
      } 

      protected virtual void Dispose(bool disposing) 
      { 
       cts.Cancel(); 

       if (disposing) 
       { 
        feedTask.Wait(); 

        feedTask.Dispose(); 

        Logger.Log("MyClassWithCancellationSourceAndUsingTaskDotRun Disposed"); 
       } 
       else 
       { 
        Logger.Log("MyClassWithCancellationSourceAndUsingTaskDotRun Finalized"); 
       } 
      } 

      ~MyClassWithCancellationSourceAndUsingTaskDotRun() 
      { 
       Dispose(false); 
      } 
     } 


     // finalizer gets called eventually (unless parent process is terminated) 
     public class MyClassWithCancellationSourceAndNoLambda : IDisposable 
     { 
      private CancellationTokenSource cts = new CancellationTokenSource(); 

      private readonly Task feedTask; 

      public MyClassWithCancellationSourceAndNoLambda() 
      { 
       Logger.Log("New MyClassWithCancellationSourceAndNoLambda Instance"); 
       feedTask = Task.Factory.StartNew(Background, cts.Token); 
      } 

      private static void Background(object state) 
      { 
       var cancelled = (CancellationToken)state; 
       if (cancelled != null) 
       { 
        int i = 0; 
        while (!cancelled.IsCancellationRequested) 
        { 
         Logger.Log("Background task for MyClassWithCancellationSourceAndNoLambda running. " + i++); 
         Thread.Sleep(1000); 
        } 
       } 
      } 

      public void Dispose() 
      { 
       Dispose(true); 
       GC.SuppressFinalize(this); 
      } 

      protected virtual void Dispose(bool disposing) 
      { 
       cts.Cancel(); 

       if (disposing) 
       { 
        feedTask.Wait(); 

        feedTask.Dispose(); 

        Logger.Log("MyClassWithCancellationSourceAndNoLambda Disposed"); 
       } 
       else 
       { 
        Logger.Log("MyClassWithCancellationSourceAndNoLambda Finalized"); 
       } 
      } 

      ~MyClassWithCancellationSourceAndNoLambda() 
      { 
       Dispose(false); 
      } 
     } 


     // finalizer doesn't get called until the app is exiting: background process keeps running 
     public class MyClassWithVolatileBoolCancellationFlag : IDisposable 
     { 
      class CancellationFlag { public volatile bool IsSet; } 

      private CancellationFlag cf = new CancellationFlag(); 

      private readonly Task feedTask; 

      public MyClassWithVolatileBoolCancellationFlag() 
      { 
       Logger.Log("New MyClassWithVolatileBoolCancellationFlag Instance"); 
       feedTask = Task.Factory.StartNew(() => 
       { 
        int i = 0; 
        while (!cf.IsSet) 
        { 
         Logger.Log("Background task for MyClassWithVolatileBoolCancellationFlag running. " + i++); 
         Thread.Sleep(1000); 
        } 
       }); 
      } 


      public void Dispose() 
      { 
       Dispose(true); 
       GC.SuppressFinalize(this); 
      } 

      protected virtual void Dispose(bool disposing) 
      { 
       cf.IsSet = true; 

       if (disposing) 
       { 
        feedTask.Wait(); 

        feedTask.Dispose(); 

        Logger.Log("MyClassWithVolatileBoolCancellationFlag Disposed"); 
       } 
       else 
       { 
        Logger.Log("MyClassWithVolatileBoolCancellationFlag Finalized"); 
       } 
      } 

      ~MyClassWithVolatileBoolCancellationFlag() 
      { 
       Dispose(false); 
      } 
     } 
    } 
} 

Update:

Добавлено несколько больше тестов (в настоящее время включены выше): и пришел к такому же выводу, как «USR»: финализации никогда не вызывается, если есть ссылка на родительский класс (что имеет смысл: существует активная ссылка, поэтому GC не срабатывает)

+1

Лямбда вызывает проблему нефинализации, поэтому этот тест показывает это. Обратите внимание, что токен не может быть доступен из финализатора (который вы не делаете). Так что возможность отсутствует. – usr

+0

Действительно, это похоже на ваш ответ @usr. Я пытаюсь сделать это с несколькими другими перестановками, обновится позже. – Nathan

+0

@usr обновлено :) – Nathan