Come progettare un IDisposable che deve essere smaltito incondizionatamente?

1

Considera una classe che implementa IDisposable e che ha membri in modo tale da non diventare mai idonea per la garbage collection quando non viene eliminata. E poiché non sarà raccolto, non avrà la possibilità di utilizzare il distruttore per la pulizia.

Di conseguenza, quando non viene smaltito (ad esempio utenti non affidabili, errori di programmazione), le risorse verranno divulgate.

Esiste un approccio generale su come una tale classe può essere progettata per affrontare una situazione del genere o per evitarla?

Esempio :

using System;

class Program
{
    static void Main(string[] args)
    {
        new Cyclical();
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.ReadKey();
    }
}

class Cyclical
{
    public Cyclical()
    {
        timer = new System.Threading.Timer(Callback, null, 0, 1000);
    }

    System.Threading.Timer timer;

    void Callback(object state)
    {
        Console.Write('.');  // do something useful
    }

    ~Cyclical()
    {
        Console.WriteLine("destructor");
    }
}

(Ometto IDisposable per mantenere un esempio breve.) Questa classe usa un Timer per fare qualcosa di utile a determinati intervalli. Ha bisogno di un riferimento al Timer per evitare che venga raccolto dei rifiuti.

Supponiamo che l'utente di quella classe non lo disponga. Come risultato del timer, da qualche parte un thread worker ha un riferimento all'istanza Cyclical tramite il callback e, di conseguenza, l'istanza Cyclical non diventerà mai idonea per la garbage collection e il suo distruttore non verrà mai eseguito e le risorse perderanno.

In questo esempio, una possibile correzione (o soluzione alternativa) potrebbe essere quella di utilizzare una classe helper che riceva callback da Timer , e che non ha un riferimento, ma solo un WeakReference all'istanza Cyclical , che chiama usando quel WeakReference .

Tuttavia, in generale, esiste una regola di progettazione per classi come questa che devono essere eliminate per evitare perdite di risorse?

Per completezza, qui l'esempio include IDispose e include un workaround / solution (e con un nome che spera meno distraente):

class SomethingWithTimer : IDisposable
{
   public SomethingWithTimer()
   {
      timer = new System.Threading.Timer(StaticCallback,
         new WeakReference<SomethingWithTimer>(this), 0, 1000);
   }

   System.Threading.Timer timer;

   static void StaticCallback(object state)
   {
      WeakReference<SomethingWithTimer> instanceRef
         = (WeakReference<SomethingWithTimer>) state;
      SomethingWithTimer instance;
      if (instanceRef.TryGetTarget(out instance))
         instance.Callback(null);
   }

   void Callback(object state)
   {
      Console.Write('.');  // do something useful
   }

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

   protected virtual void Dispose(bool disposing)
   {
      if (disposing)
      {
         Console.WriteLine("dispose");
         timer.Dispose();
      }
   }

   ~SomethingWithTimer()
   {
      Console.WriteLine("destructor");
      Dispose(false);
   }
}
  • Se disposto, il timer verrà eliminato.
  • Se non disposto, l'oggetto diventerà idoneo per la garbage collection.
posta Martin 17.04.2014 - 09:56
fonte

5 risposte

2

Un approccio che può essere utile è quello di esporre gli oggetti wrapper al mondo esterno, ma non mantenere alcun riferimento strong a loro internamente. O gli oggetti interni mantengono deboli riferimenti ai wrapper, oppure ogni involucro mantiene l'unico riferimento in qualsiasi parte del mondo a un oggetto finalizzabile che a sua volta contiene un riferimento all'oggetto "reale" [Non mi piace avere alcun esterno Gli oggetti di world-facing implementano Finalize , poiché gli oggetti non hanno il controllo su chi può chiamare cose come GC.SuppressFinalize() su di essi]. Quando tutti i riferimenti esterni all'oggetto wrapper sono scomparsi, tutti i riferimenti deboli all'oggetto wrapper verranno invalidati [e possono essere riconosciuti come tali] e qualsiasi oggetto finalizzabile a cui il wrapper mantiene l'unico riferimento eseguirà il suo Finalize metodo.

Per i gestori di eventi che sono garantiti per essere attivati su base periodica (ad esempio tick del timer) mi piace l'approccio WeakReference . Non è necessario utilizzare un finalizzatore per garantire che il timer venga pulito; invece, l'evento tick-tick può notare che il collegamento al mondo esterno è stato abbandonato e si ripulisce. Tale approccio può anche essere praticabile per cose le cui uniche risorse sono le sottoscrizioni agli eventi raramente generati da oggetti longevi (o statici), se tali oggetti hanno un evento CheckSubscription che si attiva periodicamente quando viene aggiunto un sottoscrittore (il tasso a che tali eventi dovrebbero incendiare dipende dal numero di abbonamenti). Gli abbonamenti agli eventi rappresentano un problema reale quando un numero illimitato di oggetti può iscriversi a un evento da una singola istanza di un oggetto di lunga durata e deve essere abbandonato senza annullamento dell'iscrizione; in tal caso, il numero di abbonamenti abbandonati ma non recuperabili può crescere senza vincoli. Se 100 istanze di una classe si iscrivono a un evento da un oggetto longevo e successivamente vengono abbandonate, e nient'altro sottoscrive mai quell'evento, la memoria utilizzata da tali sottoscrizioni potrebbe non essere mai ripulita, ma la quantità di memoria sprecata sarebbe limitata a quei 100 abbonamenti. L'aggiunta di più iscrizioni causerebbe a un certo punto l'evento CheckSubscription da attivare, che a sua volta causerebbe l'annullamento di tutte le sottoscrizioni degli oggetti abbandonati.

PS - Un altro vantaggio dell'utilizzo di riferimenti deboli è che si può richiedere che un WeakReference rimanga valido fino a quando l'oggetto a cui si riferisce è completamente e irrimediabilmente andato al 100%. È possibile che un oggetto appaia abbandonato e che sia pianificato il suo metodo Finalize , ma poi venga risuscitato e restituito all'utilizzo attivo anche prima che il suo metodo Finalize venga effettivamente eseguito; questo può accadere completamente al di fuori del controllo dell'oggetto. Se il codice contiene un riferimento lungo e debole all'oggetto wrapper pubblico, e se si assicura che il riferimento stesso rimanga rootato , può trattenere qualsiasi operazione di pulizia fino a quando la resurrezione diventa impossibile.

    
risposta data 14.07.2014 - 18:18
fonte
3

In circostanze normali, i riferimenti ciclici non sono un problema per il GC, perché possono rilevarli. Ma qui, l'intero grafico dell'oggetto appare come questo (basato sulla fonte di riferimento ) :

Quindi,ilproblemanonèsoloilciclo,èchehaiuncampostaticochefariferimentoalciclo.

QuellochepotrestifareperrisolvereilproblemaèinterrompereilciclotraTimerCallbackeCyclical.Neltuocodicediesempio,èsemplice:bastacambiareCallbackinunmetodostatic.Neltuocodicereale,interrompereilciclopotrebberichiederepiùlavoro,comelacreazionediunaclasseseparataperCallback,oforseanchel'usodi WeakReference .

Ciò che accade quando lo fai è quando Cyclical riceve GCed, verrà eseguito il finalizzatore di TimerHolder , che interromperà il timer.

Ma dovresti comunque stare attento a questo, perché si applicano i soliti avvertimenti per i finalizzatori: non sai mai quando verrà eseguito un finalizzatore. Soprattutto quando non stai allocando molta memoria, potrebbe volerci molto tempo prima che venga eseguito.

    
risposta data 17.04.2014 - 14:10
fonte
2

Consider a class that implements IDisposable, but when it is not disposed by its user, it will not become eligible for garbage collection, thus its destructor won’t run, and resources will be leaked.

La tua presunzione non è corretta. Tutte le classi sono disposte e un'implementazione scritta correttamente di IDisposable non perderà risorse.

Per essere assolutamente chiaro,

  • Il garbage collector alla fine raccoglie tutta la memoria GC e non libera la memoria non GC. I riferimenti ciclici sono ancora raccolti, quando disponibili.
  • Esistono IDisposable per liberare memoria non GC (e altre risorse) e non ha alcun ruolo nella liberazione della memoria GC.

Se Dispose non viene mai chiamato, tutta la memoria GC verrà comunque liberata. Un'implementazione scritta correttamente di IDisposable garantirà che anche le risorse non gestite vengano liberate, anche se Dispose non viene chiamato.

Per i dettagli vedere link . La citazione pertinente è:

Because the IDisposable.Dispose implementation is called by the consumer of a type when the resources owned by an instance are no longer needed, you should either wrap the managed object in a SafeHandle (the recommended alternative), or you should override Object.Finalize to free unmanaged resources in the event that the consumer forgets to call Dispose.

Se hai motivo di credere che non sia così, ti preghiamo di fornire un riferimento e una spiegazione del perché.

Due ulteriori punti. Non vi è alcun motivo particolare per aspettarsi che i riferimenti ciclici non possano essere raccolti. I cicli causano molti problemi per il conteggio dei riferimenti, ma il GC non presenta tali problemi. L'algoritmo ha 3 fasi (mark, relocate, compact) e trova oggetti live tracciando da 3 posizioni (stack roots, handle GC e dati statici). I cicli che non hanno radice sono semplicemente disposti.

C'è un problema qui in agguato che non è stato menzionato. Sebbene il GC garantisca l'eventuale finalizzazione di tutti gli oggetti, durante i quali verranno rilasciate risorse non GC, non garantisce l'ordine in cui ciò avverrà. Se ci sono delle dipendenze degli ordini tra le risorse non GC, fare affidamento sulla finalizzazione potrebbe causare perdite. La soluzione a ciò comporta la corretta allocazione e gestione delle risorse non GC, che non rientrano nell'ambito di questa domanda.

    
risposta data 17.04.2014 - 12:06
fonte
0

Se la tua classe contiene un campo IDisposable dovrai renderlo anche IDisposable in modo che possa essere esplicitamente Smaltire i propri campi. O altrimenti garantisci che il Dispose si verifichi eventualmente aggiungendo un metodo Finalize .

Si noti che l'uso del distruttore per la pulizia non è sufficiente perché non è garantito l'esecuzione (almeno non in java).

class Cyclical: IDisposable
{
    public Cyclical()
    {
        timer = new System.Threading.Timer(Callback, null, 0, 1000);
    }

    System.Threading.Timer timer;

    public void Dispose()
    {
        timer.Dispose();
    }

    protected virtual void Finalize()
    {
        Dispose();
    }

    void Callback(object state)
    {
        Console.Write('.');  // do something useful
    }

    ~Cyclical()
    {
        Console.WriteLine("destructor");
        Dispose();
    }
}
    
risposta data 17.04.2014 - 10:05
fonte
0

Per fare questo, prima devono essere spiegate le regole fondamentali della raccolta dei rifiuti .Net. Un oggetto è tenuto in vita mentre una delle seguenti condizioni è vera:

  • Viene fatto riferimento in un campo statico
  • Viene fatto riferimento allo stack in qualsiasi thread
  • Viene fatto riferimento a qualsiasi oggetto che è mantenuto in vita da una di queste regole

Quindi, se hai un campo statico in una classe, qualsiasi oggetto inserito in quel campo sarà mantenuto attivo fino a quando quel campo non sarà impostato su null. Se metti una raccolta, array, ect, in quel campo statico, qualsiasi oggetto nella raccolta o nell'array verrà mantenuto in vita fino a quando non viene rimosso dalla raccolta, oppure il campo è impostato su null.

Per rispondere alla tua domanda, però:

public class KeepAlive : IDisposable
{
   // Never garbage collected because its static
   private static HashSet<KeepAlive> doNotCollect = new HashSet<KeepAlive>();

   public KeepAlive()
   {
      // Right now this new object is kept alive because it's referenced on the
      // stack

      lock (doNotCollect)
         doNotCollect.Add(this);

      // Now this new object will never be collected until Dispose is called
      // because:
      // - doNotCollect isn't collected because its in a static field
      // - The new object is referenced by doNotCollect
   }

   public void Dispose()
   {
      lock (doNotCollect)
         doNotCollect.Remove(this);
   }
}

La soluzione di cui sopra funzionerà perché garantisce che il nuovo oggetto KeepAlive venga fatto riferimento in una raccolta statica fino a quando non viene chiamato Dispose (). Un piccolo avvertimento, tuttavia: Overriding Equals (), GetHashCode () o l'operatore ==, possono causare problemi. Pertanto, in questa situazione, non dovresti modificare il comportamento di equivalenza dell'oggetto.

Ma sei sicuro che questo è il comportamento che vuoi? Potrebbe essere preferibile utilizzare il garbage collector a proprio vantaggio e avvisare della perdita di risorse. Ad esempio:

public class WarnWhenLeaked : IDisposable
{
    public void Dispose()
    {
       // Prevent the GC from calling ~WarnWhenLeaked()
       GC.SurpressFinalize(this);
    }

    ~WarnWhenLeaked()
    {
       Console.WriteLine("Leaked Resource!!!");
       this.Dispose();
    }
}

Nella classe precedente, quando l'oggetto è Smaltato, la chiamata al finalizzatore (~ WarnWhenLeaked ()) è disabilitata. Pertanto, se l'oggetto è garbage collection senza chiamare Dispose (), è in grado di registrare un avviso e rilasciare la risorsa.

La situazione sopra potrebbe essere migliore di perdere definitivamente la risorsa, perché potrebbe consentire al tuo programma di recuperare da un bug.

    
risposta data 02.11.2016 - 19:04
fonte

Leggi altre domande sui tag