
Multithreading
Sprechen wir jetzt über dünnes Eis. In den vorherigen Abschnitten zu IDisposable haben wir ein sehr wichtiges Konzept angesprochen, das nicht nur den Entwurfsprinzipien von Einweg-Typen, sondern allen Typen im Allgemeinen zugrunde liegt. Dies ist das Integritätskonzept des Objekts. Dies bedeutet, dass sich ein Objekt zu einem bestimmten Zeitpunkt in einem streng festgelegten Zustand befindet und jede Aktion mit diesem Objekt seinen Zustand in eine der Optionen verwandelt, die beim Entwerfen eines Objekttyps im Voraus festgelegt wurden. Mit anderen Worten, keine Aktion mit dem Objekt sollte es in einen undefinierten Zustand versetzen. Dies führt zu einem Problem mit den in den obigen Beispielen entworfenen Typen. Sie sind nicht threadsicher. Es besteht die Möglichkeit, dass die öffentlichen Methoden dieser Art aufgerufen werden, wenn ein Objekt zerstört wird. Lassen Sie uns dieses Problem lösen und entscheiden, ob wir es überhaupt lösen sollen.
Dieses Kapitel wurde vom Autor und von professionellen Übersetzern gemeinsam aus dem Russischen übersetzt . Sie können uns bei der Übersetzung von Russisch oder Englisch in eine andere Sprache helfen, hauptsächlich ins Chinesische oder Deutsche.
Wenn Sie sich bei uns bedanken möchten, können Sie dies am besten tun, indem Sie uns einen Stern auf Github geben oder das Repository teilen
github / sidristij / dotnetbook .
public class FileWrapper : IDisposable { IntPtr _handle; bool _disposed; object _disposingSync = new object(); public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Seek(int position) { lock(_disposingSync) { CheckDisposed(); // Seek API call } } public void Dispose() { lock(_disposingSync) { if(_disposed) return; _disposed = true; } InternalDispose(); GC.SuppressFinalize(this); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { lock(_disposingSync) { if(_disposed) { throw new ObjectDisposedException(); } } } private void InternalDispose() { CloseHandle(_handle); } ~FileWrapper() { InternalDispose(); } /// other methods }
Der _disposed
Validierungscode in Dispose () sollte als kritischer Abschnitt initialisiert werden. Tatsächlich sollte der gesamte Code öffentlicher Methoden als kritischer Abschnitt initialisiert werden. Dies löst das Problem des gleichzeitigen Zugriffs auf eine öffentliche Methode eines Instanztyps und auf eine Methode ihrer Zerstörung. Es bringt jedoch andere Probleme mit sich, die zu einer Zeitbombe werden:
- Der intensive Einsatz von Typinstanzmethoden sowie das Erstellen und Zerstören von Objekten verringern die Leistung erheblich. Dies liegt daran, dass das Aufnehmen einer Sperre Zeit kostet. Diese Zeit ist erforderlich, um SyncBlockIndex-Tabellen zuzuweisen, den aktuellen Thread und viele andere Dinge zu überprüfen (wir werden sie im Kapitel über Multithreading behandeln). Das heißt, wir müssen die Leistung des Objekts während seiner gesamten Lebensdauer für die „letzte Meile“ seines Lebens opfern.
- Zusätzlicher Speicherverkehr für Synchronisationsobjekte.
- Zusätzliche Schritte, die GC ausführen sollte, um ein Objektdiagramm zu durchlaufen.
Lassen Sie uns nun die zweite und meiner Meinung nach wichtigste Sache nennen. Wir erlauben die Zerstörung eines Objekts und erwarten gleichzeitig, wieder damit zu arbeiten. Was hoffen wir in dieser Situation? dass es scheitern wird? Denn wenn Dispose zuerst ausgeführt wird, führt die folgende Verwendung von Objektmethoden definitiv zu ObjectDisposedException
. Daher sollten Sie die Synchronisierung zwischen Dispose () -Aufrufen und anderen öffentlichen Methoden eines Typs an die Serviceseite delegieren, dh an den Code, der die Instanz der FileWrapper
Klasse erstellt hat. Dies liegt daran, dass nur die erstellende Seite weiß, was sie mit einer Instanz einer Klasse tun wird und wann sie zerstört werden muss. Andererseits sollte ein Dispose-Aufruf nur kritische Fehler erzeugen, wie z. B. OutOfMemoryException
, nicht jedoch IOException. Dies liegt an den Anforderungen an die Architektur von Klassen, die IDisposable implementieren. Dies bedeutet, dass, wenn Dispose von mehr als einem Thread gleichzeitig aufgerufen wird, die Zerstörung einer Entität von zwei Threads gleichzeitig erfolgen kann (wir überspringen die Überprüfung, if(_disposed) return;
). Dies hängt von der Situation ab: Wenn eine Ressource mehrmals freigegeben werden kann , sind keine zusätzlichen Überprüfungen erforderlich. Andernfalls ist ein Schutz erforderlich:
// I don't show the whole pattern on purpose as the example will be too long // and will not show the essence class Disposable : IDisposable { private volatile int _disposed; public void Dispose() { if(Interlocked.CompareExchange(ref _disposed, 1, 0) == 0) { // dispose } } }
Zwei Ebenen des Einweg-Konstruktionsprinzips
Was ist das beliebteste Muster für die Implementierung von IDisposable
, das Sie in .NET-Büchern und im Internet finden können? Welches Muster wird von Ihnen während der Vorstellungsgespräche für einen potenziellen neuen Job erwartet? Höchstwahrscheinlich dieser:
public class Disposable : IDisposable { bool _disposed; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if(disposing) { // here we release managed resources } // here we release unmanaged resources } protected void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } ~Disposable() { Dispose(false); } }
Was ist falsch an diesem Beispiel und warum haben wir noch nie so geschrieben? In der Tat ist dies ein gutes Muster, das für alle Situationen geeignet ist. Die allgegenwärtige Verwendung ist meiner Meinung nach jedoch kein guter Stil, da wir in der Praxis fast nicht mit nicht verwalteten Ressourcen umgehen, sodass die Hälfte des Musters keinen Zweck erfüllt. Da es gleichzeitig sowohl verwaltete als auch nicht verwaltete Ressourcen verwaltet, verstößt es außerdem gegen das Prinzip der Aufteilung der Verantwortung. Ich denke das ist falsch. Schauen wir uns einen etwas anderen Ansatz an. Prinzip des Einwegdesigns . Kurz gesagt, es funktioniert wie folgt:
Die Entsorgung ist in zwei Klassenstufen unterteilt:
- Level 0-Typen kapseln direkt nicht verwaltete Ressourcen
- Sie sind entweder abstrakt oder verpackt.
- Alle Methoden sollten markiert sein:
- PrePrepareMethod, damit beim Laden eines Typs eine Methode kompiliert werden kann
- SecuritySafeCritical zum Schutz vor einem Aufruf aus dem Code unter Einschränkungen
- ReliabilityContract (Consistency.WillNotCorruptState, Cer.Success / MayFail)], um CER für eine Methode und alle ihre untergeordneten Aufrufe zu setzen
- Sie können auf Typen der Ebene 0 verweisen, sollten jedoch den Zähler für die Referenzierung von Objekten erhöhen, um die richtige Reihenfolge für die Eingabe der „letzten Meile“ zu gewährleisten.
- Level 1-Typen kapseln nur verwaltete Ressourcen
- Sie werden nur von Level 1-Typen geerbt oder implementieren IDisposable direkt
- Sie können keine Level 0-Typen oder CriticalFinalizerObject erben
- Sie können verwaltete Typen der Ebenen 1 und 0 kapseln
- Sie implementieren IDisposable. Entsorgen Sie, indem Sie gekapselte Objekte ab Level 0-Typen zerstören und zu Level 1 wechseln
- Sie implementieren keinen Finalizer, da sie sich nicht mit nicht verwalteten Ressourcen befassen
- Sie sollten eine geschützte Eigenschaft enthalten, die den Zugriff auf Level 0-Typen ermöglicht.
Aus diesem Grund habe ich die Unterteilung von Anfang an in zwei Typen verwendet: den mit einer verwalteten Ressource und den mit nicht verwalteten Ressourcen. Sie sollten anders funktionieren.
Andere Verwendungsmöglichkeiten Entsorgen
Die Idee hinter der Erstellung von IDisposable war die Freigabe nicht verwalteter Ressourcen. Aber wie bei vielen anderen Mustern ist es sehr hilfreich für andere Aufgaben, z. B. Verweise auf verwaltete Ressourcen freizugeben. Die Freigabe verwalteter Ressourcen klingt jedoch nicht sehr hilfreich. Ich meine, sie werden absichtlich als verwaltet bezeichnet, damit wir uns mit einem Grinsen in Bezug auf C / C ++ - Entwickler entspannen können, oder? Dies ist jedoch nicht der Fall. Es kann immer vorkommen, dass wir einen Verweis auf ein Objekt verlieren, aber gleichzeitig denken, dass alles in Ordnung ist: GC sammelt Müll, einschließlich unseres Objekts. Es stellt sich jedoch heraus, dass der Speicher wächst. Wir steigen in das Speicheranalyseprogramm ein und sehen, dass etwas anderes dieses Objekt enthält. Die Sache ist, dass es eine Logik für die implizite Erfassung eines Verweises auf Ihre Entität sowohl in der .NET-Plattform als auch in der Architektur externer Klassen geben kann. Da die Erfassung implizit ist, kann ein Programmierer die Notwendigkeit seiner Freigabe übersehen und dann einen Speicherverlust erhalten.
Delegierte, Veranstaltungen
Schauen wir uns dieses synthetische Beispiel an:
class Secondary { Action _action; void SaveForUseInFuture(Action action) { _action = action; } public void CallAction() { _action(); } } class Primary { Secondary _foo = new Secondary(); public void PlanSayHello() { _foo.SaveForUseInFuture(Strategy); } public void SayHello() { _foo.CallAction(); } void Strategy() { Console.WriteLine("Hello!"); } }
Welches Problem zeigt dieser Code? Die sekundäre Klasse speichert den Delegaten des _action
Feld _action
, der in der SaveForUseInFuture
Methode akzeptiert wird. Als Nächstes PlanSayHello
Methode in der PlanSayHello
den Zeiger auf die Strategy
an die Secondary
. Es ist merkwürdig, aber wenn Sie in diesem Beispiel irgendwo eine statische Methode oder eine Instanzmethode übergeben, wird die übergebene SaveForUseInFuture
nicht geändert, aber eine Instanz der SaveForUseInFuture
wird implizit oder überhaupt nicht referenziert. Äußerlich scheinen Sie angewiesen zu haben, welche Methode aufgerufen werden soll. Tatsächlich wird ein Delegat jedoch nicht nur mithilfe eines Methodenzeigers erstellt, sondern auch mithilfe des Zeigers auf eine Instanz einer Klasse. Ein anrufender Teilnehmer sollte verstehen, für welche Instanz einer Klasse er die Strategy
aufrufen muss! Dies ist die Instanz der Secondary
, die implizit akzeptiert wurde und den Zeiger auf die Instanz der Primary
, obwohl dies nicht explizit angegeben ist. Für uns bedeutet dies nur, dass GC kein Primary
Objekt sammelt , wenn wir den _foo
Zeiger an einer anderen Stelle übergeben und den Verweis auf Primary
verlieren, da Secondary
ihn enthält. Wie können wir solche Situationen vermeiden? Wir brauchen einen entschlossenen Ansatz, um einen Verweis auf uns zu veröffentlichen. Ein Mechanismus, der perfekt zu diesem Zweck passt, ist IDisposable
// This is a simplified implementation class Secondary : IDisposable { Action _action; public event Action<Secondary> OnDisposed; public void SaveForUseInFuture(Action action) { _action = action; } public void CallAction() { _action?.Invoke(); } void Dispose() { _action = null; OnDisposed?.Invoke(this); } }
Jetzt sieht das Beispiel akzeptabel aus. Wenn eine Instanz einer Klasse an einen Dritten übergeben wird und der Verweis auf _action
delegate während dieses Vorgangs verloren geht, setzen wir ihn auf Null und der Dritte wird über die Zerstörung der Instanz benachrichtigt und der Verweis darauf gelöscht .
Die zweite Gefahr von Code, der auf Delegierten ausgeführt wird, sind die Funktionsprinzipien des event
. Schauen wir uns an, was daraus resultiert:
// a private field of a handler private Action<Secondary> _event; // add/remove methods are marked as [MethodImpl(MethodImplOptions.Synchronized)] // that is similar to lock(this) public event Action<Secondary> OnDisposed { add { lock(this) { _event += value; } } remove { lock(this) { _event -= value; } } }
C # -Nachrichten verbergen die Interna von Ereignissen und enthalten alle Objekte, die für die Aktualisierung über ein event
abonniert wurden. Wenn etwas schief geht, bleibt ein Verweis auf ein signiertes Objekt in OnDisposed
und enthält das Objekt. Es ist eine seltsame Situation, da wir in Bezug auf die Architektur ein Konzept der „Ereignisquelle“ erhalten, das logisch nichts enthalten sollte. Tatsächlich werden Objekte, die für die Aktualisierung abonniert wurden, implizit gehalten. Darüber hinaus können wir in diesem Array von Delegaten nichts ändern, obwohl die Entität uns gehört. Das einzige, was wir tun können, ist, diese Liste zu löschen, indem wir einer Ereignisquelle null zuweisen.
Die zweite Möglichkeit besteht darin remove
Methoden zum add
/ remove
explizit zu implementieren, damit wir eine Sammlung von Delegaten steuern können.
Eine andere implizite Situation kann hier auftreten. Wenn Sie einer Ereignisquelle null zuweisen, führt das folgende Abonnement für Ereignisse möglicherweise zu einer NullReferenceException
. Ich denke, das wäre logischer.
Dies ist jedoch nicht wahr. Wenn externer Code Ereignisse abonniert, nachdem eine Ereignisquelle gelöscht wurde, erstellt FCL eine neue Instanz der Aktionsklasse und speichert sie in OnDisposed
. Diese implizite Aussage in C # kann einen Programmierer irreführen: Der Umgang mit nullen Feldern sollte eher eine Art Wachsamkeit als Ruhe erzeugen. Hier zeigen wir auch einen Ansatz, bei dem die Nachlässigkeit eines Programmierers zu Speicherlecks führen kann.
Lambdas Verschlüsse
Die Verwendung von syntaktischem Zucker wie Lambdas ist besonders gefährlich.
Ich möchte den syntaktischen Zucker als Ganzes ansprechen. Ich denke, Sie sollten es ziemlich vorsichtig und nur verwenden, wenn Sie das Ergebnis genau kennen. Beispiele für Lambda-Ausdrücke sind Verschlüsse, Verschlüsse in Ausdrücken und viele andere Leiden, die Sie sich selbst zufügen können.
Natürlich können Sie sagen, dass Sie wissen, dass ein Lambda-Ausdruck einen Abschluss erzeugt und zu einem Risiko eines Ressourcenlecks führen kann. Aber es ist so ordentlich, so angenehm, dass es schwer zu vermeiden ist, Lambda zu verwenden, anstatt die gesamte Methode zuzuweisen, die an einem anderen Ort als dem, an dem sie verwendet wird, beschrieben wird. Tatsächlich sollten Sie sich dieser Provokation nicht anschließen, obwohl nicht jeder widerstehen kann. Schauen wir uns das Beispiel an:
button.Clicked += () => service.SendMessageAsync(MessageType.Deploy);
Stimmen Sie zu, diese Linie sieht sehr sicher aus. Aber es verbirgt ein großes Problem: Jetzt verweist die button
implizit auf den service
und hält ihn. Selbst wenn wir entscheiden, dass wir keinen service
mehr benötigen, enthält die button
die Referenz, solange diese Variable aktiv ist. Eine Möglichkeit, dieses Problem zu lösen, besteht darin, ein Muster zum Erstellen von IDisposable
aus einer beliebigen Action
( System.Reactive.Disposables
) zu verwenden:
// Here we create a delegate from a lambda Action action = () => service.SendMessageAsync(MessageType.Deploy); // Here we subscribe button.Clicked += action; // We unsubscribe var subscription = Disposable.Create(() => button.Clicked -= action); // where it is necessary subscription.Dispose();
Zugegeben, das sieht etwas langwierig aus und wir verlieren den ganzen Zweck der Verwendung von Lambda-Ausdrücken. Es ist viel sicherer und einfacher, gängige private Methoden zu verwenden, um Variablen implizit zu erfassen.
Threadabort-Schutz
Wenn Sie eine Bibliothek für einen Drittanbieter erstellen, können Sie deren Verhalten in einer Drittanbieteranwendung nicht vorhersagen. Manchmal können Sie nur raten, was ein Programmierer mit Ihrer Bibliothek gemacht hat, was zu einem bestimmten Ergebnis geführt hat. Ein Beispiel ist das Funktionieren in einer Multithread-Umgebung, in der die Konsistenz der Ressourcenbereinigung zu einem kritischen Problem werden kann. Beachten Sie, dass wir beim Schreiben der Dispose()
-Methode das Fehlen von Ausnahmen garantieren können. Wir können jedoch nicht sicherstellen, dass beim Ausführen der Dispose()
-Methode keine ThreadAbortException
auftritt, die unseren Ausführungsthread deaktiviert. Hier sollten wir uns daran erinnern, dass beim ThreadAbortException
ohnehin alle catch / finally-Blöcke ausgeführt werden (am Ende eines catch / finally-Blocks tritt ThreadAbort weiter unten auf). Um die Ausführung eines bestimmten Codes mithilfe von Thread.Abort sicherzustellen, müssen Sie einen kritischen Abschnitt in try { ... } finally { ... }
einschließen. Siehe das folgende Beispiel:
void Dispose() { if(_disposed) return; _someInstance.Unsubscribe(this); _disposed = true; }
Mit Thread.Abort
kann man dies jederzeit Thread.Abort
. Es zerstört teilweise ein Objekt, obwohl Sie auch in Zukunft damit arbeiten können. Zur gleichen Zeit der folgende Code:
void Dispose() { if(_disposed) return; // ThreadAbortException protection try {} finally { _someInstance.Unsubscribe(this); _disposed = true; } }
ist vor einem solchen Abbruch geschützt und läuft reibungslos und sicher, auch wenn Thread.Abort
zwischen dem Aufrufen der Unsubscribe
Methode und der Ausführung ihrer Anweisungen Unsubscribe
wird.
Ergebnisse
Vorteile
Nun, wir haben viel über dieses einfachste Muster gelernt. Lassen Sie uns seine Vorteile bestimmen:
- Der Hauptvorteil des Musters ist die Fähigkeit, Ressourcen bestimmt freizugeben, dh wenn Sie sie benötigen.
- Der zweite Vorteil ist die Einführung einer bewährten Methode, um zu überprüfen, ob eine bestimmte Instanz ihre Instanzen nach der Verwendung zerstören muss.
- Wenn Sie das Muster korrekt implementieren, funktioniert ein entworfener Typ sicher in Bezug auf die Verwendung durch Komponenten von Drittanbietern sowie in Bezug auf das Entladen und Zerstören von Ressourcen, wenn ein Prozess abstürzt (z. B. aufgrund von Speichermangel). Dies ist der letzte Vorteil.
Nachteile
Meiner Meinung nach hat dieses Muster mehr Nachteile als Vorteile.
- Ein Typ weist jeden Typ, der dieses Muster implementiert, andere Teile an, dass sie, wenn sie es verwenden, eine Art öffentliches Angebot annehmen. Dies ist so implizit, dass ein Benutzer eines Typs wie bei öffentlichen Angeboten nicht immer weiß, dass der Typ über diese Schnittstelle verfügt. Daher müssen Sie den IDE-Anweisungen folgen (geben Sie einen Punkt ein, Dis ... und prüfen Sie, ob die gefilterte Mitgliederliste einer Klasse eine Methode enthält). Wenn Sie ein Dispose-Muster sehen, sollten Sie es in Ihren Code implementieren. Manchmal passiert es nicht sofort und in diesem Fall sollten Sie ein Muster durch ein System von Typen implementieren, das Funktionen hinzufügt. Ein gutes Beispiel ist, dass
IEnumerator<T>
IDisposable
. - Wenn Sie eine Schnittstelle entwerfen, müssen Sie normalerweise IDisposable in das System der Schnittstellen eines Typs einfügen, wenn eine der Schnittstellen IDisposable erben muss. Meiner Meinung nach beschädigt dies die von uns entworfenen Schnittstellen. Ich meine, wenn Sie eine Schnittstelle entwerfen, erstellen Sie zuerst ein Interaktionsprotokoll. Dies ist eine Reihe von Aktionen, die Sie mit etwas ausführen können, das sich hinter der Benutzeroberfläche verbirgt.
Dispose()
ist eine Methode zum Zerstören einer Instanz einer Klasse. Dies widerspricht dem Wesen eines Interaktionsprotokolls . Tatsächlich sind dies die Details der Implementierung, die in die Schnittstelle eingedrungen sind. - Dispose () bedeutet trotz seiner Bestimmung nicht die direkte Zerstörung eines Objekts. Das Objekt wird nach seiner Zerstörung noch existieren, jedoch in einem anderen Zustand. Um dies zu erreichen, muss CheckDisposed () der erste Befehl jeder öffentlichen Methode sein. Dies sieht nach einer vorübergehenden Lösung aus, die uns jemand mit den Worten gegeben hat: „Geh hinaus und vermehr dich“;
- Es gibt auch eine kleine Chance, einen Typ zu erhalten, der
IDisposable
durch explizite Implementierung implementiert. Oder Sie können einen Typ erhalten, der die ID implementiert, ohne dass Sie bestimmen können, wer sie zerstören muss: Sie oder die Partei, die sie Ihnen gegeben hat. Dies führte zu einem Antimuster mehrerer Aufrufe von Dispose (), mit dem ein zerstörtes Objekt zerstört werden kann. - Die vollständige Implementierung ist schwierig und unterscheidet sich für verwaltete und nicht verwaltete Ressourcen. Hier erscheint der Versuch, die Arbeit der Entwickler durch GC zu erleichtern, umständlich. Sie können die
virtual void Dispose()
-Methode überschreiben und einen DisposableObject-Typ einführen, der das gesamte Muster implementiert, andere mit dem Muster verbundene Probleme jedoch nicht löst. - In der Regel wird die Dispose () -Methode am Ende einer Datei implementiert, während '.ctor' am Anfang deklariert wird. Wenn Sie eine Klasse ändern oder neue Ressourcen einführen, können Sie leicht vergessen, die Entsorgung für diese hinzuzufügen.
- Schließlich ist es schwierig, die Reihenfolge der Zerstörung in einer Multithread-Umgebung zu bestimmen, wenn Sie ein Muster für Objektdiagramme verwenden, in denen Objekte dieses Muster ganz oder teilweise implementieren. Ich meine Situationen, in denen Dispose () an verschiedenen Enden eines Diagramms beginnen kann. Hier ist es besser, andere Muster zu verwenden, z. B. das Lifetime-Muster.
- Der Wunsch der Plattformentwickler, die Speichersteuerung in Kombination mit der Realität zu automatisieren: Anwendungen interagieren sehr oft mit nicht verwaltetem Code. + Sie müssen die Freigabe von Verweisen auf Objekte steuern, damit Garbage Collector sie erfassen kann. Dies führt zu großer Verwirrung beim Verständnis von Fragen wie: „Wie sollen wir ein Muster richtig implementieren?“. "Gibt es überhaupt ein verlässliches Muster"? Vielleicht
delete obj; delete[] arr;
delete obj; delete[] arr;
ist einfacher?
Entladen der Domäne und Beenden einer Anwendung
Wenn Sie zu diesem Teil gekommen sind, haben Sie mehr Vertrauen in den Erfolg zukünftiger Vorstellungsgespräche. Wir haben jedoch nicht alle Fragen besprochen, die mit diesem einfachen Muster verbunden sind. Die letzte Frage ist, ob sich das Verhalten einer Anwendung bei einfacher Speicherbereinigung unterscheidet und wenn beim Entladen der Domäne und beim Beenden der Anwendung Speicherplatz gesammelt wird. Diese Frage berührt lediglich Dispose()
... Dispose()
und Finalisierung gehen jedoch Hand in Hand, und wir treffen selten auf eine Implementierung einer Klasse, die Finalisierung hat, aber keine Dispose()
-Methode hat. Beschreiben wir die Finalisierung in einem separaten Abschnitt. Hier fügen wir nur einige wichtige Details hinzu.
Während des Entladens der Anwendungsdomäne entladen Sie sowohl in die Anwendungsdomäne geladene Assemblys als auch alle Objekte, die als Teil der zu entladenden Domäne erstellt wurden. Tatsächlich bedeutet dies die Bereinigung (Sammlung durch GC) dieser Objekte und das Aufrufen von Finalisierern für sie. Wenn die Logik eines Finalizers darauf wartet, dass die Finalisierung anderer Objekte in der richtigen Reihenfolge zerstört wird, achten Sie möglicherweise auf die Eigenschaft Environment.HasShutdownStarted
angibt, dass eine Anwendung aus dem Speicher entladen wurde, und auf die Methode AppDomain.CurrentDomain.IsFinalizingForUnload()
, die dies angibt Domain wird entladen, was der Grund für die Finalisierung ist. Wenn diese Ereignisse eintreten, wird die Reihenfolge der Ressourcenabschluss im Allgemeinen unwichtig. Wir können weder das Entladen einer Domain noch einer Anwendung verzögern, da wir alles so schnell wie möglich erledigen sollten.
Auf diese Weise wird diese Aufgabe als Teil einer Klasse LoaderAllocatorScout gelöst
// Assemblies and LoaderAllocators will be cleaned up during AppDomain shutdown in // an unmanaged code // So it is ok to skip reregistration and cleanup for finalization during appdomain shutdown. // We also avoid early finalization of LoaderAllocatorScout due to AD unload when the object was inside DelayedFinalizationList. if (!Environment.HasShutdownStarted && !AppDomain.CurrentDomain.IsFinalizingForUnload()) { // Destroy returns false if the managed LoaderAllocator is still alive. if (!Destroy(m_nativeLoaderAllocator)) { // Somebody might have been holding a reference on us via weak handle. // We will keep trying. It will be hopefully released eventually. GC.ReRegisterForFinalize(this); } }
Typische Implementierungsfehler
Wie ich Ihnen gezeigt habe, gibt es kein universelles Muster für die Implementierung von IDisposable. Darüber hinaus führt ein gewisses Vertrauen in die automatische Speichersteuerung Menschen in die Irre und sie treffen verwirrende Entscheidungen bei der Implementierung eines Musters. Das gesamte .NET Framework ist mit Fehlern in seiner Implementierung durchsetzt. Schauen wir uns diese Fehler am Beispiel von .NET Framework genau an, um meinen Standpunkt zu belegen. Alle Implementierungen sind verfügbar über: IDisposable Usages
FileEntry-Klasse cmsinterop.cs
Dieser Code wurde in Eile geschrieben, um das Problem zu schließen. Offensichtlich wollte der Autor etwas tun, überlegte es sich aber anders und behielt eine fehlerhafte Lösung bei
internal class FileEntry : IDisposable { // Other fields // ... [MarshalAs(UnmanagedType.SysInt)] public IntPtr HashValue; // ... ~FileEntry() { Dispose(false); } // The implementation is hidden and complicates calling the *right* version of a method. void IDisposable.Dispose() { this.Dispose(true); } // Choosing a public method is a serious mistake that allows for incorrect destruction of // an instance of a class. Moreover, you CANNOT call this method from the outside public void Dispose(bool fDisposing) { if (HashValue != IntPtr.Zero) { Marshal.FreeCoTaskMem(HashValue); HashValue = IntPtr.Zero; } if (fDisposing) { if( MuiMapping != null) { MuiMapping.Dispose(true); MuiMapping = null; } System.GC.SuppressFinalize(this); } } }
SemaphoreSlim Class System / Threading / SemaphoreSlim.cs
Dieser Fehler steht ganz oben auf den Fehlern von .NET Framework in Bezug auf IDisposable: SuppressFinalize für Klassen, in denen kein Finalizer vorhanden ist. Es ist sehr häufig.
public void Dispose() { Dispose(true); // As the class doesn't have a finalizer, there is no need in GC.SuppressFinalize GC.SuppressFinalize(this); } // The implementation of this pattern assumes the finalizer exists. But it doesn't. // It was possible to do with just public virtual void Dispose() protected virtual void Dispose(bool disposing) { if (disposing) { if (m_waitHandle != null) { m_waitHandle.Close(); m_waitHandle = null; } m_lockObj = null; m_asyncHead = null; m_asyncTail = null; } }
Aufruf von Close + Dispose NativeWatcher-Projektcode
Manchmal nennen die Leute sowohl Schließen als auch Entsorgen. Dies ist falsch, führt jedoch nicht zu einem Fehler, da bei der zweiten Entsorgung keine Ausnahme generiert wird.
In der Tat ist Close ein weiteres Muster, um die Dinge für die Menschen klarer zu machen. Es machte jedoch alles unklarer.
public void Dispose() { if (MainForm != null) { MainForm.Close(); MainForm.Dispose(); } MainForm = null; }
Allgemeine Ergebnisse
- IDposable ist ein Standard der Plattform und die Qualität ihrer Implementierung beeinflusst die Qualität der gesamten Anwendung. Darüber hinaus beeinflusst es in bestimmten Situationen die Sicherheit Ihrer Anwendung, die über nicht verwaltete Ressourcen angegriffen werden kann.
- Die Implementierung von IDisposable muss maximal produktiv sein. Dies gilt insbesondere für den Abschnitt der Finalisierung, der parallel zum Rest des Codes arbeitet und Garbage Collector lädt.
- Bei der Implementierung von IDisposable sollten Sie Dispose () nicht gleichzeitig mit öffentlichen Methoden einer Klasse verwenden. Die Zerstörung kann nicht mit der Nutzung einhergehen. Dies sollte beim Entwerfen eines Typs berücksichtigt werden, der ein IDisposable-Objekt verwendet.
- Es sollte jedoch ein Schutz gegen das gleichzeitige Aufrufen von 'Dispose ()' von zwei Threads bestehen. Dies ergibt sich aus der Anweisung, dass Dispose () keine Fehler erzeugen sollte.
- Typen, die nicht verwaltete Ressourcen enthalten, sollten von anderen Typen getrennt werden. Ich meine, wenn Sie eine nicht verwaltete Ressource umschließen, sollten Sie einen separaten Typ dafür zuweisen. Dieser Typ sollte die Finalisierung enthalten und von
SafeHandle / CriticalHandle / CriticalFinalizerObject
geerbt werden. Diese Aufgabentrennung führt zu einer verbesserten Unterstützung des Typsystems und vereinfacht die Implementierung, um Instanzen von Typen über Dispose () zu zerstören: Die Typen mit dieser Implementierung müssen keinen Finalizer implementieren. - Im Allgemeinen ist dieses Muster sowohl bei der Verwendung als auch bei der Codepflege nicht komfortabel. Wahrscheinlich sollten wir den Inversion of Control-Ansatz verwenden, wenn wir den Status von Objekten über das
Lifetime
Muster zerstören. Wir werden jedoch im nächsten Abschnitt darüber sprechen.
Dieses Kapitel wurde vom Autor und von professionellen Übersetzern gemeinsam aus dem Russischen übersetzt . Sie können uns bei der Übersetzung von Russisch oder Englisch in eine andere Sprache helfen, hauptsächlich ins Chinesische oder Deutsche.
Wenn Sie sich bei uns bedanken möchten, können Sie dies am besten tun, indem Sie uns einen Stern auf Github geben oder das Repository teilen
github / sidristij / dotnetbook .