2012-11-03 7 views
5

A volte gli utenti desiderano programmare un ampio set di timer e non vogliono gestire i riferimenti a quei timer.
Nel caso in cui l'utente non faccia riferimento a un timer, il timer può essere raccolto dal GC prima dell'esecuzione.
Ho creato il Timer classe a fungere da supporto per luogo timer appena creati:Perdita di memoria attorno al delegato del timer

static class Timers 
{ 
    private static readonly ILog _logger = LogManager.GetLogger(typeof(Timers)); 

    private static readonly ConcurrentDictionary<Object, Timer> _timers = new ConcurrentDictionary<Object, Timer>(); 

    /// <summary> 
    /// Use this class in case you want someone to hold a reference to the timer. 
    /// Timer without someone referencing it will be collected by the GC even before execution. 
    /// </summary> 
    /// <param name="dueTime"></param> 
    /// <param name="action"></param> 
    internal static void ScheduleOnce(TimeSpan dueTime, Action action) 
    { 
     if (dueTime <= TimeSpan.Zero) 
     { 
      throw new ArgumentOutOfRangeException("dueTime", dueTime, "DueTime can only be greater than zero."); 
     } 
     Object obj = new Object(); 

     Timer timer = new Timer(state => 
     { 
      try 
      { 
       action(); 
      } 
      catch (Exception ex) 
      { 
       _logger.ErrorFormat("Exception while executing timer. ex: {0}", ex); 
      } 
      finally 
      { 
       Timer removedTimer; 
       if (!_timers.TryRemove(obj, out removedTimer)) 
       { 
        _logger.Error("Failed to remove timer from timers"); 
       } 
       else 
       { 
        removedTimer.Dispose(); 
       } 
      } 
     }); 
     if (!_timers.TryAdd(obj, timer)) 
     { 
      _logger.Error("Failed to add timer to timers"); 
     } 
     timer.Change(dueTime, TimeSpan.FromMilliseconds(-1)); 
    } 
} 

Se non dispongo il timer rimosso, risulta con una perdita di memoria.
Sembra che qualcuno abbia un riferimento al delegato del Timer dopo che il timer è stato rimosso dalla raccolta _timers.

La domanda è: perché ricevo una perdita di memoria se non dispongo il timer?

+0

Forse non capisco quello che stai chiedendo, perché suona come vi state chiedendo perché qualcosa si comporta male quando si maltrattano esso ... –

+0

Capisco che la documentazione indichi che i componenti devono essere smaltiti. Sono ancora curioso di sapere cosa impedisce al GC di raccogliere il timer e il delegato dato senza chiamare il metodo di smaltimento. –

risposta

9

Il Timer viene tenuto in vita da uno GCHandle creato dal timer stesso. Questo può essere testato usando un profiler di memoria .net. A sua volta lo Timer manterrà in vita il delegato, che manterrà in vita il resto.

Un GCHandle è un tipo speciale di oggetto che può essere utilizzato per "ingannare" il Garbage Collector per mantenere in vita oggetti non raggiungibili.

Si potrebbe in realtà tipo-di test di questo senza un profiler utilizzando:

var a = new ClassA(); 
var timer = new Timer(a.Exec); 

var refA = new WeakReference(a); 
var refTimer = new WeakReference(timer); 

a = null; 
timer = null; 

GC.Collect(); 
GC.WaitForPendingFinalizers(); 
GC.Collect(); 

Console.WriteLine(refA.IsAlive); 
Console.WriteLine(refTimer.IsAlive); 
+0

È interessante. Mi chiedo perché la documentazione di System.Threading.Timer dice: "Finché si utilizza un Timer, è necessario mantenere un riferimento ad esso. Come in ogni oggetto gestito, un Timer è soggetto alla garbage collection quando non ci sono riferimenti ad esso Il fatto che un Timer sia ancora attivo non impedisce che venga raccolto. " Ho anche notato che il timer non può essere eseguito se non viene mantenuto alcun riferimento al timer. –

+0

Sembra che questo fosse il modo in cui gli sviluppatori intendevano farlo funzionare. Se eseguo il mio programma di test in .net 4.0 o versioni successive, sembra che gli oggetti vengano raccolti. –

4

Timers sono Components. Come tale è necessario chiamare Dispose quando hai finito con loro.

Da the documentation:

Un Componente dovrebbe rilasciare le risorse in modo esplicito da chiamate verso il suo metodo Dispose, senza aspettare la gestione automatica della memoria attraverso una chiamata implicita al metodo Finalize. Quando viene smaltito uno Container, vengono eliminati anche tutti i componenti all'interno di Container.

La parte "Quando un Container è disposto, tutti i componenti all'interno Container sono anche disposti." può essere visto in metodo Dispose di un modulo quando si chiama:

if (disposing && (components != null)) 
{ 
    components.Dispose(); 
} 

quindi non aspettatevi i timer da smaltire con il modulo a meno che non sono stati aggiunti ai componenti.

aggiornamento per il tuo commento:
Un timer ha puntatori a codice non gestito (del sistema operativo del timer API) in modo che non può disporre fino a che quelli non sono più necessari. Il finalizzatore non verrà eseguito sull'oggetto a meno che non venga chiamato dispose per primo o che il programma stia uscendo. Questo a causa di questi riferimenti attuali al codice non gestito.

Da quello che ho capito, il modello di smaltimento doveva accelerare la chiusura dei programmi (poiché il tempo di esecuzione poteva raccogliere il cestino durante i tempi di fermo) pur consentendo l'esecuzione di codice non gestito. Se fai un gran numero di importazioni di ddl inizierai a vedere perché il sistema funziona come fa.

Si noti inoltre che la documentazione indica che non è possibile accedere agli oggetti gestiti dal finalizzatore di un oggetto. Un esempio di questo è un StreamWriter. Personalmente penso che questa sia una regola arbitraria, ma esiste, quindi la necessità del sistema di smaltimento.

In entrambi i casi, se si utilizza qualcosa che implementa l'interfaccia iDisposable, si dovrebbe sempre disporre di esso quando si è fatto con esso. Otterrai risultati migliori (più consistenti) in questo modo.

+0

Giusto. Ma il timer verrà raccolto dal GC e quindi chiamerà il finalizzatore che rilascerà il delegato e il resto delle risorse. La mia domanda è perché in questo caso è necessario il metodo Dispose. –

+0

@OronNadiv Un timer ha puntatori al codice non gestito (l'API del timer del sistema operativo) in modo che non possa essere eliminato finché non sono più necessari. il finalizzatore non verrà eseguito sull'oggetto a meno che non venga chiamato prima o il programma stia uscendo a causa di questi riferimenti correnti al codice non gestito. – Trisped

+1

Sembra che stia usando 'System.Threading.Timer', che non è un' Componente'. –