Come ottenere la notifica prima che le variabili statiche siano finalizzate

Quando posso ripulire gli oggetti memorizzati in variabili statiche in C #?

Ho una variabile statica che è pigramente inizializzata :

public class Sqm { private static Lazy _default = new Lazy(); public static Sqm Default { get { return _default.Value; } } } 

Nota : ho appena cambiato Foo per essere una class static . Non cambia la domanda in alcun modo se Foo è statico o meno. Ma alcune persone sono convinte che non c’è modo che un’istanza di Sqm possa essere costruita senza prima build un’istanza di Foo . Anche se ho creato un object Foo ; anche se ne avessi creati 100, non mi avrebbe aiutato a risolvere il problema (di quando “ripulire” un membro statico).

Esempio di utilizzo

 Foo.Default.TimerStart("SaveQuestion"); //...snip... Foo.Default.TimerStop("SaveQuestion"); 

Ora, la mia class Sqm implementa un metodo che deve essere chiamato quando l’object non è più necessario e deve ripulirsi (salvare lo stato nel sistema di archiviazione, rilasciare i blocchi, ecc.). Questo metodo deve essere chiamato prima dell’esecuzione dei garbage collector (cioè prima che venga chiamato il finalizzatore del mio object):

 public class Sqm { var values = new List(); Boolean shutdown = false; protected void Cleanup(ICollection stuff) { WebRequest http = new HttpWebRequest(); http.Open("POST", "https://stackoverflow.com/SubmitUsageTelemetry"); http.PostBody = stuff; http.Send(); } public void Shutdown() { if (!alreadyShutdown) { Cleanup(values); alreadyShutdown = true; } } } 

Quando e dove posso chiamare il mio metodo Shutdown() ?

Nota : non voglio che lo sviluppatore che usa la class Sqm debba preoccupare di chiamare Shutdown . Non è il suo lavoro. In altri ambienti linguistici non avrebbe dovuto.

La class Lazy non sembra chiamare Dispose sul Value che possiede pigramente. Quindi non posso agganciare lo schema IDisposable e usarlo come il tempo per chiamare Shutdown . Devo chiamare Shutdown me stesso.

Ma quando?

È una variabile static , esiste una volta per la vita dell’applicazione / dominio / appdominio / appartamento.

Sì, il finalizzatore è il momento sbagliato

Alcune persone capiscono, e alcune persone no, che provare a caricare i miei dati durante un finalizer è sbagliato .

 ///WRONG: Don't do this! ~Sqm { Shutdown(_values); //<-- BAD! _values might already have been finalized by the GC! } 

Perché è sbagliato? Perché i values potrebbero non esserci più. Non controlli quali oggetti sono finalizzati in quale ordine. È del tutto ansible che i values stati finalizzati prima dello Sqm contenente.

Che dire di smaltire?

L’interfaccia IDisposable e il metodo Dispose() è una convenzione . Non c’è nulla che imponga che se il mio object implementa un metodo Dispose() che sarà mai chiamato. In effetti, potrei andare avanti e implementarlo:

 public class Sqm : IDisposable { var values = new List(); Boolean alreadyDiposed = false; protected void Cleanup(ICollection stuff) { WebRequest http = new HttpWebRequest(); http.Open("POST", "https://stackoverflow.com/SubmitUsageTelemetry"); http.PostBody = stuff; http.Send(); } public void Dispose() { if (!alreadyDiposed) { Cleanup(values); alreadyDiposed = true; } } } 

Alla persona che sta effettivamente leggendo la domanda, potresti notare che in realtà non ho cambiato nulla. L’unica cosa che ho fatto è stato cambiare il nome di un metodo da Shutdown a Dispose . Il modello Dispose è semplicemente una convenzione. ho ancora il problema: quando posso chiamare Dispose ?

Bene, dovresti chiamare il tuo finalizzatore

Chiamare Dispose dal mio finalizzatore è sbagliato come chiamare Shutdown dal mio finalizzatore (sono identicamente sbagliati):

 public class Sqm : IDisposable { var values = new List(); Boolean alreadyDiposed = false; protected void Cleanup(ICollection stuff) { WebRequest http = new HttpWebRequest(); http.Open("POST", "https://stackoverflow.com/SubmitUsageTelemetry"); http.PostBody = stuff; http.Send(); } public void Dispose() { if (!alreadyDiposed) { Cleanup(_values); // <--BUG: _values might already have been finalized by the GC! alreadyDiposed = true; } } ~Sqm { Dispose(); } } 

Perché, ancora, i values potrebbero non esserci più. Per completezza, possiamo ritornare al codice corretto originale completo:

 public class Sqm : IDisposable { var values = new List(); Boolean alreadyDiposed = false; protected void Cleanup(ICollection stuff) { WebRequest http = new HttpWebRequest(); http.Open("POST", "https://stackoverflow.com/SubmitUsageTelemetry"); http.PostBody = stuff; http.Send(); } protected void Dispose(Boolean itIsSafeToAlsoAccessManagedResources) { if (!alreadyDiposed) { if (itIsSafeToAlsoAccessManagedResources) Cleanup(values); alreadyDiposed = true; } } public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } ~Sqm { Dispose(false); //false ==> it is not safe to access values } } 

Sono tornato al punto di partenza. Ho un object che ho bisogno di “ripulire” prima che il dominio dell’applicazione si arresti. Qualcosa all’interno del mio object deve essere notificato quando può chiamare Cleanup .

Fai in modo che lo sviluppatore lo chiami

No.

sto migrando concetti esistenti da un’altra lingua in C #. Se uno sviluppatore utilizza l’istanza globale singleton:

 Foo.Sqm.TimerStart(); 

quindi la class Sqm è pigra inizializzata. In un’applicazione (nativa), viene mantenuto il riferimento all’object. Durante l’arresto dell’applicazione (nativo), la variabile che contiene il puntatore dell’interfaccia è impostata su null e viene chiamato il destructor dell’object singleton e può pulirsi da solo.

Nessuno dovrebbe mai chiamare nulla. Non Cleanup , non Shutdown , non Dispose . L’arresto dovrebbe avvenire automaticamente dall’infrastruttura.

Qual è l’equivalente in C # di me stesso che mi vedo andare via, pulirmi ?

È complicato dal fatto che se si lascia che il garbage collector raccolga l’object: è troppo tardi. Gli oggetti di stato interni che voglio persistere sono probabilmente già finalizzati.

Sarebbe facile se da ASP.net

Se potessi garantire che la mia class venisse utilizzata da ASP.net, potrei chiedere a HostingEnvironment di avvisare prima che il dominio si HostingEnvironment registrando il mio object con esso:

 System.Web.Hosting.HostingEnvironment.RegisterObject(this); 

E implementa il metodo Stop :

 public class Sqm : IDisposable, IRegisteredObject { var values = new List(); Boolean alreadyDiposed = false; protected void Cleanup(ICollection stuff) { WebRequest http = new HttpWebRequest(); http.Open("POST", "https://stackoverflow.com/SubmitUsageTelemetry"); http.PostBody = stuff; http.Send(); } protected void Dispose(Boolean itIsSafeToAlsoAccessManagedResources) { if (!alreadyDiposed) { if (itIsSafeToAlsoAccessManagedResources) Cleanup(values); alreadyDiposed = true; } } public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } Sqm { //Register ourself with the ASP.net hosting environment, //so we can be notified with the application is shutting down HostingEnvironment.RegisterObject(this); //asp.net will call Stop() when it's time to cleanup } ~Sqm { Dispose(false); //false ==> it is not safe to access values } // IRegisteredObject protected void Stop(Boolean immediate) { if (immediate) { //i took too long to shut down; the rug is being pulled out from under me. //i had my chance. Oh well. return; } Cleanup(); //or Dispose(), both good } } 

Tranne la mia class non sa se sarò chiamato da ASP.net , o da WinForms , o da WPF , o un’applicazione per console, o un’estensione della shell.

Modifica : le persone sembrano essere confuse da ciò per cui esiste il modello IDisposable . Rimossi i riferimenti a Dispose per rimuovere la confusione.

Modifica 2 : le persone sembrano richiedere un codice di esempio completo e dettagliato prima di rispondere alla domanda. Personalmente penso che la domanda contenga già troppo codice di esempio, in quanto non serve a porre la domanda.

E ora che ho aggiunto molto codice, la domanda è andata persa. Le persone si rifiutano di rispondere a una domanda finché la domanda non è stata giustificata. Ora che è stato giustificato, nessuno lo leggerà.

È come la diagnostica

È come la class System.Diagnostics.Trace . Le persone lo chiamano quando vogliono:

 Trace.WriteLine("Column sort: {0} ms", sortTimeInMs); 

e non pensarci più.

E poi inizia la disperazione

ero anche abbastanza disperato, che ho considerato di hide il mio object dietro un’interfaccia COM IUnknown , che è il conteggio di riferimento

 public class Sqm : IUnknown { IUnknown _default = new Lazy(); } 

E poi spero di poter ingannare il CLR nel decrementare il conteggio dei riferimenti sulla mia interfaccia. Quando il mio conteggio dei riferimenti diventa zero, so che tutto si sta spegnendo.

Il rovescio della medaglia è che non riesco a farlo funzionare.

Ci sono due problemi qui:

  • Sei insistente sul fatto che l’ List potrebbe essere stato finalizzato. List non ha un finalizzatore e non è stato ancora raccolto (poiché ci si riferisce ad esso). (Quelle sono operazioni diverse.) Il tuo finalizzatore SQL vedrà comunque dati validi. Quindi, in realtà, un finalizzatore potrebbe essere a posto – anche se nel momento in cui il finalizzatore esegue alcune altre risorse che potrebbero essere sparite – e il finalizzatore potrebbe anche non essere chiamato. Quindi penso che questo sia allo stesso tempo più fattibile di quello che ti aspetti – e un’idea peggio in generale.

  • Sei insistente nel dire che non vuoi rendere questo deterministico mettendolo sotto il controllo dello sviluppatore, indipendentemente dal fatto che IDisposable usando IDisposable o meno. Questo è semplicemente combattendo contro ciò che fornisce .NET. Il garbage collector è pensato per le risorse di memoria ; tutte le risorse non di memoria che richiedono una pulizia deterministica (incluso lo svuotamento, ecc.) dovrebbero essere ripulite esplicitamente. È ansible utilizzare un finalizzatore come ultima pulizia “best effort”, ma non dovrebbe essere usato nel modo in cui si sta tentando di usarlo.

Esistono alcuni approcci che è ansible utilizzare per cercare di aggirare questo problema, ad esempio utilizzare un object “canarino” con un riferimento all’object “reale”: mantenere un forte riferimento all’object che si è interessato altrove e disporre di un finalizzatore solo nell’object canarino, in modo che l’ unica cosa da finalizzare sia l’object canarino – che quindi triggers il flush appropriato e rimuove l’ultimo riferimento forte, lasciando l’object reale idoneo per GC – ma è fondamentalmente una ctriggers idea, e con variabili statiche nel mix diventa anche peggio.

Allo stesso modo è ansible utilizzare l’evento AppDomain.DomainUnload , ma ancora una volta non lo farei. Quando il dominio verrà scaricato, sarei preoccupato per lo stato del resto degli oggetti e non verrà chiamato per il dominio predefinito.

Fondamentalmente, penso che dovresti cambiare il tuo design. Non conosciamo lo sfondo dell’API che stai cercando di progettare, ma il modo in cui stai andando al momento non funzionerà. Cercherei di evitare la variabile statica, personalmente – almeno per tutto ciò che è importante in termini di tempo. Potrebbe esserci ancora un singolo object dietro le quinte per il coordinamento, ma esponendo ciò nella tua API mi sembra un errore. Per quanto tu protesti riguardo ad altre lingue e altre piattaforms, se stai lavorando in .NET devi accettare che è quello che è. Combattere contro il sistema non ti aiuterà a lungo termine.

Prima si è arrivati ​​alla conclusione che è necessario modificare la progettazione dell’API, più tempo si deve pensare a come dovrebbe apparire la nuova API.

C’è un evento ProcessExit sull’AppDomain che potresti provare ad agganciare, anche se non ne so molto altro, e ha un limite di tempo predefinito di 2 secondi.

Qualcosa di simile (se è adatto a te);

 class SQM { static Lazy _Instance = new Lazy( CreateInstance ); private static SQM CreateInstance() { AppDomain.CurrentDomain.ProcessExit += new EventHandler( Cleanup ); return new SQM(); } private static Cleanup() { ... } } 

Oltre alla risposta di Ken, la risposta a “Come posso smaltire il mio object?” è, non puoi.

Il concetto che stai cercando è un decostruttore statico, o un decostruttore che verrebbe eseguito quando i metodi statici vengono liberati. Questo non esiste nel codice gestito e nella maggior parte dei casi (tutti?) Non dovrebbe essere necessario. È più che probabile che i metodi statici vengano scaricati quando l’eseguibile finisce e il sistema operativo pulirà tutto a quel punto.

Se è assolutamente necessario liberare risorse e questo object deve essere condiviso tra tutte le istanze attive, è ansible creare un contatore di riferimento e smaltire l’object quando si è sicuri che tutti i riferimenti sono stati rilasciati. Prenderò in seria considerazione se questo sia l’approccio corretto per te per primo. Le nuove istanze dovrebbero verificare se il tuo object è null e istanziarlo di nuovo se è così.

Non dovresti aver bisogno di chiamare Dispose . Se la class che implementa IDisposable utilizza solo risorse gestite, tali risorse verranno rilasciate come faranno naturalmente al termine del programma. Se la class utilizza risorse non gestite, tale class deve estendere CriticalFinalizerObject e liberare tali risorse nel suo finalizzatore (nonché nel suo metodo Dispose ).

In altre parole, l’uso corretto dell’interfaccia IDisposable non richiede che venga mai chiamato Dispose . Può essere chiamato per rilasciare risorse gestite o non gestite in un punto particolare del programma, ma una perdita che si verifica a causa del non chiamarlo dovrebbe essere considerata un bug.

modificare

Qual è l’equivalente in C # di me stesso che mi vedo andare via, pulirmi ?

In risposta alla tua domanda modificata, penso che tu stia cercando il finalizzatore:

 class Foo { ~Foo() { // Finalizer code. Called when garbage collected, maybe... } } 

Siate consapevoli, tuttavia, che non vi è alcuna garanzia che questo metodo venga chiamato. Se è assolutamente necessario chiamare, è necessario estendere System.Runtime.ConstrainedExecution.CriticalFinalizerObject .

Potrei comunque essere confuso dalla tua domanda, comunque. Il finalizzatore NON è sicuramente il luogo in cui “salvare i miei valori interni in un file”.

L’ evento AppDomain Domain Unload sembra essere adatto per quello che stai cercando. Dal momento che le variabili statiche persistono fino a quando AppDomain non viene scaricato, questo dovrebbe darti un aghook giusto prima che la variabile venga distrutta.

Stai passando tutto questo tempo a combattere la lingua, perché non riprogettare in modo che il problema non esista?

Ad esempio, se è necessario salvare lo stato di una variabile, anziché cercare di catturarla prima che venga distrutta, salvarla ogni volta che viene modificata e sovrascrivere lo stato precedente.

avevo fatto questa domanda quattro volte, se in quattro modi diversi. Phrasing ciascuno leggermente diverso; cercando di risolvere il problema da una direzione diversa. Alla fine è stato MA Hanin a indicarmi questa domanda che ha risolto il problema.

Il problema è che non esiste un unico modo per sapere quando il dominio si sta spegnendo. Il meglio che puoi fare è provare a catturare vari tipi di eventi che ti coprono al 100% (arrotondato alla percentuale più vicina) del tempo.

Se il codice si trova in un dominio diverso da quello predefinito, utilizzare l’evento DomainUnload . Sfortunatamente l’AppDomain predefinito non DomainUnload un evento DomainUnload . Quindi prendiamo ProcessExit :

 class InternalSqm { //constructor public InternalSqm () { //... //Catch domain shutdown (Hack: frantically look for things we can catch) if (AppDomain.CurrentDomain.IsDefaultAppDomain()) AppDomain.CurrentDomain.ProcessExit += MyTerminationHandler; else AppDomain.CurrentDomain.DomainUnload += MyTerminationHandler; } private void MyTerminationHandler(object sender, EventArgs e) { //The domain is dying. Serialize out our values this.Dispose(); } ... } 

Questo è stato testato all’interno di un “sito web” e di un’applicazione “WinForms” .

Il codice più completo, che mostra un’implementazione di IDisposable :

 class InternalSqm : IDisposable { private Boolean _disposed = false; //constructor public InternalSqm() { //... //Catch domain shutdown (Hack: frantically look for things we can catch) if (AppDomain.CurrentDomain.IsDefaultAppDomain()) AppDomain.CurrentDomain.ProcessExit += MyTerminationHandler; else AppDomain.CurrentDomain.DomainUnload += MyTerminationHandler; } private void MyTerminationHandler(object sender, EventArgs e) { //The domain is dying. Serialize out our values this.Dispose(); } 

.

  ///  /// Finalizer (Finalizer uses the C++ destructor syntax) ///  ~InternalSqm() { Dispose(false); //False: it's not safe to access managed members } public void Dispose() { this.Dispose(true); //True; it is safe to access managed members GC.SuppressFinalize(this); //Garbage collector doesn't need to bother to call finalize later } protected virtual void Dispose(Boolean safeToAccessManagedResources) { if (_disposed) return; //be resilient to double calls to Dispose try { if (safeToAccessManagedResources) { // Free other state (managed objects). this.CloseSession(); //save internal stuff to persistent storage } // Free your own state (unmanaged objects). // Set large fields to null. Etc. } finally { _disposed = true; } } } 

Esempio di utilizzo

Da una libreria che esegue l’elaborazione delle immagini:

 public static class GraphicsLibrary { public Image RotateImage(Image image, Double angleInDegrees) { Sqm.TimerStart("GraphicaLibrary.RotateImage"); ... Sqm.TimerStop("GraphicaLibrary.RotateImage"); } } 

Da una class helper in grado di eseguire una query

 public static class DataHelper { public IDataReader ExecuteQuery(IDbConnection conn, String sql) { Sqm.TimerStart("DataHelper_ExecuteQuery"); ... Sqm.TimerStop("DataHelper_ExecuteQuery"); } } 

Per il disegno a tema WinForms

 public static class ThemeLib { public void DrawButton(Graphics g, Rectangle r, String text) { Sqm.AddToAverage("ThemeLib/DrawButton/TextLength", text.Length); } } 

In un sito web:

 private void GetUser(HttpSessionState session) { LoginUser user = (LoginUser)session["currentUser"]; if (user != null) Sqm.Increment("GetUser_UserAlreadyFoundInSession", 1); ... } 

In un metodo di estensione

 ///  /// Convert the guid to a quoted string ///  /// A Guid to convert to a quoted string ///  public static string ToQuotedStr(this Guid source) { String s = "'" + source.ToString("B") + "'"; //B=braces format "{6CC82DE0-F45D-4ED1-8FAB-5C23DE0FF64C}" //Record how often we dealt with each type of UUID Sqm.Increment("String.ToQuotedStr_UUIDType_"+s[16], 1); return s; } 

Nota : qualsiasi codice è rilasciato nel pubblico dominio. Nessuna attribuzione richiesta.

Essi permarranno per la durata di AppDomain. Le modifiche apportate alla variabile statica sono visibili attraverso i metodi.

MSDN:

Se una variabile locale è dichiarata con la parola chiave statica, la sua durata è più lunga del tempo di esecuzione della procedura in cui è dichiarata. Se la procedura è all’interno di un modulo, la variabile statica sopravvive finché l’applicazione continua a funzionare.