2016-04-06 15 views
3

Ich habe eine Klasse, die eine Aufgabe startet und sicherstellen möchte, dass die Aufgabe beendet wird, wenn das Objekt als Garbage Collection erfasst wird.Abbrechen einer Aufgabe, wenn ein Objekt abgeschlossen ist

Ich habe das IDisposable-Muster implementiert, um sicherzustellen, dass, wenn das Objekt manuell entsorgt wird oder innerhalb eines Benutzungsblocks verwendet wird, die Aufgabe ordnungsgemäß beendet wird. Allerdings, ich kann nicht garantieren, dass der Endbenutzer Dispose() aufrufen oder das Objekt innerhalb eines Verwendungsblocks verwenden wird. Ich weiß, dass der Garbage Collector letztendlich den Finalizer aufrufen wird - bedeutet das, dass die Aufgabe ausgeführt wird?

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

Es wurde in this question empfohlen, einen flüchtigen Bool hinzuzufügen, die von den Finalizer und beobachtet von der Aufgabe festgelegt ist. Wird das empfohlen, oder gibt es einen besseren Weg, um das zu erreichen, was ich brauche?

(Ich verwende .NET 4 daher die Verwendung von TaskFactory.StartNew statt Task.Run)

EDIT:

Um einige Kontext auf die Frage zu geben - was nicht wirklich gezeigt wird Im obigen Code-Snippet: Ich erstelle eine Netzwerk-Client-Klasse, die einen Mechanismus zum Leben erhält, indem regelmäßig Pakete an den Server gesendet werden. Ich habe mich entschieden, dieses Detail nicht in das Beispiel aufzunehmen, da es für meine spezifische Frage nicht relevant war. Was ich jedoch tatsächlich möchte, ist die Möglichkeit für den Benutzer, eine boolesche KeepAlive-Eigenschaft auf "true" zu setzen, wodurch eine Aufgabe gestartet wird, um alle 60 Sekunden Daten an den Server zu senden. Wenn der Benutzer die Eigenschaft auf false setzt, wird die Aufgabe beendet. IDisposable hat mich zu 90% dorthin gebracht, aber es hängt davon ab, dass der Benutzer es ordnungsgemäß entsorgt (explizit oder über die Nutzung). Ich möchte keine Keep-Alive-Aufgaben für den Benutzer offenlegen, damit sie explizit abbrechen. Ich möchte nur ein "einfaches" KeepAlive = true/false, um die Aufgabe zu starten/stoppen UND ich möchte, dass die Aufgabe beendet wird, wenn der Benutzer fertig ist das Objekt - auch wenn sie es nicht ordnungsgemäß entsorgen. Ich fange an zu denken, dass dies nicht möglich ist!

+0

Warum nicht 'System.Timers.Timer' verwenden? –

+2

Dieser Code wird im Finalizer nichts bewirken. Das ist ein Fehler. Da die Aufgabe über den Schließungszustand der äußeren MyClass-Instanz gehalten wird, wird dieses Objekt niemals finalisiert, bis die Aufgabe auf natürliche Weise beendet wird. – usr

+0

@Danny Chen Ich dachte das - aber hat das das gleiche Problem? Wie ich es verstehe, würde der Timer weiter laufen, selbst wenn das Objekt, das es erstellt hat, entsorgt wird (bitte korrigieren Sie mich, wenn ich falsch liege)! Ich überlegte, AutoReset auf "false" zu setzen und die Callback-Methode manuell jedesmal neu zu setzen, wenn es "tickt" - aber dann war ich nicht sicher, ob der GC das Objekt jemals fertigstellen würde, wenn er ein Ereignis abonniert hat. Irgendwelche Vorschläge würden geschätzt! –

Antwort

2

Ich werde eine Antwort skizzieren. Ich bin nicht 100% zuversichtlich, dass dies funktionieren wird. Finalisierung ist ein kompliziertes Problem, und ich bin nicht darin geübt.

  1. Es kann keine Objektreferenz von der Aufgabe zu dem Objekt geben, das finalisiert werden soll.
  2. Sie können andere Objekte aus einem Finalizer nicht berühren, von denen nicht bekannt ist, dass sie sicher sind. Die integrierten .NET-Klassen dokumentieren diese Sicherheitseigenschaft normalerweise nicht. Darauf können Sie sich (normalerweise) nicht verlassen.
class CancellationFlag { public volatile bool IsSet; } 

Sie können nun eine Instanz dieser Klasse zwischen der Aufgabe und MyClass teilen. Die Aufgabe muss die Flagge abfragen und MyClass muss sie setzen.

Um sicherzustellen, dass die Aufgabe nicht versehentlich das äußere Objekt verweist würde ich den Code wie folgt strukturiert:

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

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

diese Weise können Sie ausdrücklich jeder Staat durch state fädeln kann. Dies wäre zumindest ein Fall von CancellationFlag, aber unter keinen Umständen ein Verweis auf MyClass.

+0

Ich würde es ähnlich machen, außer dass ich die volatile bool flag direkt in die konsumierende Klasse setzen würde, da dies die Notwendigkeit der Betrachtung ausschließen würde, ob 'CancellationFlag' sicher ist oder nicht (auch wenn es in diesem Fall trivial ist). . – Douglas

+0

Was meinst du mit dem Konsum von Klasse? Er kann es nicht in MyClass platzieren, da dies die Finalisierung verhindert. Wenn Sie die Klasse meinen, die er wahrscheinlich für 'state' erstellen muss, stimme ich zu. – usr

+0

Sie haben Recht; Ich habe die Schließung wieder verpasst. Ich denke, das ist der beste Weg, es zu tun. – Douglas

1

Ich habe das folgende Programm erstellt, um die Unterschiede zu untersuchen ...

Aus meinen Beobachtungen, es macht keinen Unterschied, ob es ein Abbruch-Token oder eine volatile Bool ist, was wirklich wichtig ist, dass die Task.StartNew-Methode nicht mit einem Lambda-Ausdruck aufgerufen wird.

Edit: zu klären: Wenn das Lambda auf eine statische Methode bezieht, es ist tatsächlich gut: das Problem kommt, wenn das Lambda eine Referenz auf das enthaltenden Klasse verursacht enthalten sein: so entweder eine Referenz auf eine Elementvariable die Elternklasse oder sonst eine Referenz auf eine Instanzmethode der Elternklasse.

Bitte versuchen Sie es und lassen Sie mich wissen, wenn Sie zu der gleichen Schlussfolgerung kommen.

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:

Added ein paar mehr Tests (jetzt oben im Lieferumfang enthalten): und kam zu dem gleichen Schluss wie „usr“: die Finalizerthread nie aufgerufen wird, wenn ein Verweis auf die übergeordnete Klasse gibt es (was sinnvoll ist: eine aktive Referenz existiert, deshalb springt der GC nicht ein)

+1

Das Lambda verursacht das Nicht-Finalisierungsproblem, also zeigt dieser Test das. Beachten Sie, dass auf das Token nicht vom Finalizer aus zugegriffen werden kann (was Sie nicht tun). Also diese Möglichkeit ist out. – usr

+0

Tatsächlich scheint es deine Antwort @usr zu unterstützen. Ich versuche es mit ein paar anderen Permutationen, werde später aktualisieren. – Nathan

+0

@usr aktualisiert :) – Nathan