Einwegmuster (Prinzip des Einwegdesigns) Punkt 1


Einwegmuster (Prinzip des Einwegdesigns)


Ich denke, fast jeder Programmierer, der .NET verwendet, wird jetzt sagen, dass dieses Muster ein Kinderspiel ist. Dass es das bekannteste Muster ist, das auf der Plattform verwendet wird. Selbst die einfachste und bekannteste Problemdomäne verfügt jedoch über geheime Bereiche, die Sie noch nie angesehen haben. Beschreiben wir also das Ganze von Anfang an für die Anfänger und alle anderen (damit sich jeder von Ihnen an die Grundlagen erinnern kann). Überspringen Sie diese Absätze nicht - ich beobachte Sie!


Wenn ich frage, was IDisposable ist, werden Sie sicherlich sagen, dass es ist


public interface IDisposable { void Dispose(); } 

Was ist der Zweck der Schnittstelle? Ich meine, warum müssen wir überhaupt Speicher löschen, wenn wir einen intelligenten Garbage Collector haben, der den Speicher anstelle von uns löscht, sodass wir nicht einmal darüber nachdenken müssen. Es gibt jedoch einige kleine Details.


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 .

Es besteht ein Missverständnis, dass IDisposable dient, nicht verwaltete Ressourcen freizugeben. Dies ist nur teilweise richtig und um es zu verstehen, müssen Sie sich nur an die Beispiele für nicht verwaltete Ressourcen erinnern. Ist die File eine nicht verwaltete Ressource? Nein, nein. Vielleicht ist DbContext eine nicht verwaltete Ressource? Nein, schon wieder. Eine nicht verwaltete Ressource gehört nicht zum System vom Typ .NET. Etwas, das die Plattform nicht geschaffen hat, etwas, das außerhalb ihres Anwendungsbereichs existiert. Ein einfaches Beispiel ist ein geöffnetes Dateihandle in einem Betriebssystem. Ein Handle ist eine Nummer, die eine Datei eindeutig identifiziert, die von einem Betriebssystem geöffnet wurde - nein, nicht von Ihnen. Das heißt, alle Kontrollstrukturen (z. B. die Position einer Datei in einem Dateisystem, Dateifragmente im Falle einer Fragmentierung und andere Serviceinformationen, die Nummern eines Zylinders, eines Kopfes oder eines Sektors einer Festplatte) befinden sich innerhalb eines Betriebssystems, jedoch nicht .NET-Plattform. Die einzige nicht verwaltete Ressource, die an die .NET-Plattform übergeben wird, ist die IntPtr-Nummer. Diese Nummer wird von FileSafeHandle umschlossen, das wiederum von der File-Klasse umschlossen wird. Dies bedeutet, dass die File-Klasse keine eigenständige nicht verwaltete Ressource ist, sondern eine zusätzliche Ebene in Form von IntPtr verwendet, um eine nicht verwaltete Ressource einzuschließen - das Handle einer geöffneten Datei. Wie liest du diese Datei? Verwenden einer Reihe von Methoden unter WinAPI oder Linux.


Synchronisationsprimitive in Multithread- oder Multiprozessorprogrammen sind das zweite Beispiel für nicht verwaltete Ressourcen. Hierher gehören Datenarrays, die über P / Invoke geleitet werden, sowie Mutexe oder Semaphoren.


Beachten Sie, dass das Betriebssystem das Handle einer nicht verwalteten Ressource nicht einfach an eine Anwendung übergibt. Außerdem wird dieses Handle in der Tabelle der vom Prozess geöffneten Handles gespeichert. Somit kann das Betriebssystem die Ressourcen nach Beendigung der Anwendung korrekt schließen. Dadurch wird sichergestellt, dass die Ressourcen nach dem Beenden der Anwendung trotzdem geschlossen werden. Die Laufzeit einer Anwendung kann jedoch unterschiedlich sein, was zu einer langen Ressourcensperrung führen kann.

Ok Jetzt haben wir nicht verwaltete Ressourcen behandelt. Warum müssen wir in diesen Fällen IDisposable verwenden? Weil .NET Framework keine Ahnung hat, was außerhalb seines Territoriums vor sich geht. Wenn Sie eine Datei mit der OS-API öffnen, weiß .NET nichts darüber. Wenn Sie einen Speicherbereich für Ihre eigenen Anforderungen zuweisen (z. B. mithilfe von VirtualAlloc), weiß .NET auch nichts. Wenn es nicht weiß, wird der von einem VirtualAlloc-Aufruf belegte Speicher nicht freigegeben. Oder es wird keine Datei geschlossen, die direkt über einen OS-API-Aufruf geöffnet wurde. Diese können unterschiedliche und unerwartete Folgen haben. Sie können OutOfMemory erhalten, wenn Sie zu viel Speicher zuweisen, ohne ihn freizugeben (z. B. indem Sie einen Zeiger auf null setzen). Wenn Sie eine Datei auf einer Dateifreigabe über das Betriebssystem öffnen, ohne sie zu schließen, wird die Datei auf dieser Dateifreigabe für lange Zeit gesperrt. Das Beispiel für die Dateifreigabe ist besonders gut, da die Sperre auch nach dem Schließen einer Verbindung mit einem Server auf der IIS-Seite verbleibt. Sie haben keine Rechte zum iisreset der Sperre und müssen Administratoren iisreset , iisreset durchzuführen oder Ressourcen manuell mit einer speziellen Software zu schließen.
Dieses Problem auf einem Remote-Server kann zu einer komplexen Aufgabe werden.


Alle diese Fälle benötigen ein universelles und bekanntes Protokoll für die Interaktion zwischen einem Typsystem und einem Programmierer. Es sollte eindeutig die Typen identifizieren, die ein erzwungenes Schließen erfordern. Die IDisposable-Schnittstelle dient genau diesem Zweck. Es funktioniert folgendermaßen: Wenn ein Typ die Implementierung der IDisposable-Schnittstelle enthält, müssen Sie Dispose () aufrufen, nachdem Sie die Arbeit mit einer Instanz dieses Typs beendet haben.


Es gibt also zwei Standardmethoden, um es zu nennen. Normalerweise erstellen Sie eine Entitätsinstanz, um sie innerhalb einer Methode oder innerhalb der Lebensdauer der Entitätsinstanz schnell zu verwenden.


Die erste Möglichkeit besteht darin, eine Instanz in using(...){ ... } zu verpacken. Dies bedeutet, dass Sie anweisen, ein Objekt zu zerstören, nachdem der verwendungsbezogene Block beendet ist, dh Dispose () aufzurufen. Die zweite Möglichkeit besteht darin, das Objekt nach Ablauf seiner Lebensdauer mit einem Verweis auf das Objekt zu zerstören, das wir freigeben möchten. Aber .NET hat nichts als eine Finalisierungsmethode, die die automatische Zerstörung eines Objekts impliziert, oder? Die Finalisierung ist jedoch überhaupt nicht geeignet, da wir nicht wissen, wann sie aufgerufen wird. In der Zwischenzeit müssen wir ein Objekt zu einem bestimmten Zeitpunkt freigeben, beispielsweise kurz nachdem wir die Arbeit mit einer geöffneten Datei beendet haben. Aus diesem Grund müssen wir auch IDisposable implementieren und Dispose aufrufen, um alle Ressourcen freizugeben, die wir besaßen. Daher folgen wir dem Protokoll und es ist sehr wichtig. Denn wenn jemand ihm folgt, sollten alle Teilnehmer dasselbe tun, um Probleme zu vermeiden.


Verschiedene Möglichkeiten zur Implementierung von IDisposable


Schauen wir uns die Implementierungen von IDisposable von einfach bis kompliziert an. Das erste und einfachste ist die Verwendung von IDisposable wie es ist:


 public class ResourceHolder : IDisposable { DisposableResource _anotherResource = new DisposableResource(); public void Dispose() { _anotherResource.Dispose(); } } 

Hier erstellen wir eine Instanz einer Ressource, die von Dispose () weiter freigegeben wird. Das einzige, was diese Implementierung inkonsistent macht, ist, dass Sie nach der Zerstörung durch Dispose() immer noch mit der Instanz arbeiten können:


 public class ResourceHolder : IDisposable { private DisposableResource _anotherResource = new DisposableResource(); private bool _disposed; public void Dispose() { if(_disposed) return; _anotherResource.Dispose(); _disposed = true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } } 

CheckDisposed () muss in allen öffentlichen Methoden einer Klasse als erster Ausdruck aufgerufen werden. Die erhaltene ResourceHolder Klassenstruktur sieht gut aus, um eine nicht verwaltete Ressource zu zerstören, nämlich DisposableResource . Diese Struktur ist jedoch nicht für eine eingebettete nicht verwaltete Ressource geeignet. Schauen wir uns das Beispiel mit einer nicht verwalteten Ressource an.


 public class FileWrapper : IDisposable { IntPtr _handle; public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Dispose() { CloseHandle(_handle); } [DllImport("kernel32.dll", EntryPoint = "CreateFile", SetLastError = true)] private static extern IntPtr CreateFile(String lpFileName, UInt32 dwDesiredAccess, UInt32 dwShareMode, IntPtr lpSecurityAttributes, UInt32 dwCreationDisposition, UInt32 dwFlagsAndAttributes, IntPtr hTemplateFile); [DllImport("kernel32.dll", SetLastError=true)] private static extern bool CloseHandle(IntPtr hObject); } 

Was ist der Unterschied im Verhalten der letzten beiden Beispiele? Der erste beschreibt das Zusammenspiel zweier verwalteter Ressourcen. Das heißt, wenn ein Programm korrekt funktioniert, wird die Ressource trotzdem freigegeben. Da DisposableResource verwaltet wird, weiß .NET CLR davon und gibt den Speicher frei, wenn sein Verhalten falsch ist. Beachten Sie, dass ich bewusst nicht davon DisposableResource , welchen DisposableResource Typ kapselt. Es kann jede Art von Logik und Struktur geben. Es kann sowohl verwaltete als auch nicht verwaltete Ressourcen enthalten. Das sollte uns überhaupt nicht betreffen . Niemand fordert uns auf, die Bibliotheken von Drittanbietern jedes Mal zu dekompilieren und zu prüfen, ob sie verwaltete oder nicht verwaltete Ressourcen verwenden. Und wenn unser Typ eine nicht verwaltete Ressource verwendet, können wir uns dessen nicht bewusst sein. Wir machen das in der FileWrapper Klasse. Was passiert also in diesem Fall? Wenn wir nicht verwaltete Ressourcen verwenden, gibt es zwei Szenarien. Der erste ist, wenn alles in Ordnung ist und Dispose aufgerufen wird. Der zweite ist, wenn etwas schief geht und Dispose fehlgeschlagen ist.


Sagen wir gleich, warum dies schief gehen kann:


  • Wenn wir using(obj) { ... } , kann eine Ausnahme in einem inneren Codeblock auftreten. Diese Ausnahme wird von finally block abgefangen, was wir nicht sehen können (dies ist syntaktischer Zucker von C #). Dieser Block ruft implizit Dispose auf. Es gibt jedoch Fälle, in denen dies nicht der Fall ist. Beispiel: StackOverflowException wird weder catch noch finally StackOverflowException . Sie sollten sich immer daran erinnern. Wenn ein Thread rekursiv wird und irgendwann eine StackOverflowException auftritt, vergisst .NET die Ressourcen, die verwendet, aber nicht freigegeben wurden. Es kann keine nicht verwalteten Ressourcen freigeben. Sie bleiben im Speicher, bis das Betriebssystem sie freigibt, d. H. Wenn Sie ein Programm beenden oder sogar einige Zeit nach dem Beenden einer Anwendung.
  • Wenn wir Dispose () von einem anderen Dispose () aus aufrufen. Auch hier kann es vorkommen, dass wir nicht dazu kommen. Dies ist nicht der Fall eines abwesenden App-Entwicklers, der vergessen hat, Dispose () aufzurufen. Es ist die Frage der Ausnahmen. Dies sind jedoch nicht nur die Ausnahmen, die einen Thread einer Anwendung zum Absturz bringen. Hier sprechen wir über alle Ausnahmen, die verhindern, dass ein Algorithmus eine externe Dispose () aufruft, die unsere Dispose () aufruft.

In all diesen Fällen werden angehaltene, nicht verwaltete Ressourcen erstellt. Das liegt daran, dass Garbage Collector nicht weiß, dass er sie sammeln soll. Bei der nächsten Überprüfung kann FileWrapper festgestellt werden, dass der letzte Verweis auf ein Objektdiagramm mit unserem FileWrapper Typ verloren geht. In diesem Fall wird der Speicher für Objekte mit Referenzen neu zugewiesen. Wie können wir das verhindern?


Wir müssen den Finalizer eines Objekts implementieren. Der 'Finalizer' wird absichtlich so genannt. Es ist kein Destruktor, wie es scheinen mag, weil Finalizer in C # und Destruktoren in C ++ auf ähnliche Weise aufgerufen werden. Der Unterschied besteht darin, dass ein Finalizer im Gegensatz zu einem Destruktor (sowie Dispose() ) trotzdem aufgerufen wird . Ein Finalizer wird aufgerufen, wenn die Garbage Collection gestartet wird (jetzt reicht es aus, dies zu wissen, aber die Dinge sind etwas komplizierter). Es wird für eine garantierte Freigabe von Ressourcen verwendet, wenn etwas schief geht . Wir müssen einen Finalizer implementieren, um nicht verwaltete Ressourcen freizugeben. Da ein Finalizer aufgerufen wird, wenn GC gestartet wird, wissen wir nicht, wann dies im Allgemeinen geschieht.


Lassen Sie uns unseren Code erweitern:


 public class FileWrapper : IDisposable { IntPtr _handle; public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Dispose() { InternalDispose(); GC.SuppressFinalize(this); } private void InternalDispose() { CloseHandle(_handle); } ~FileWrapper() { InternalDispose(); } /// other methods } 

Wir haben das Beispiel mit dem Wissen über den Finalisierungsprozess erweitert und die Anwendung vor dem Verlust von Ressourceninformationen geschützt, wenn Dispose () nicht aufgerufen wird. Wir haben auch GC. SuppressFinalize aufgerufen, um die Finalisierung der Instanz des Typs zu deaktivieren, wenn Dispose () erfolgreich aufgerufen wird. Es ist nicht nötig, dieselbe Ressource zweimal freizugeben, oder? Daher reduzieren wir auch die Finalisierungswarteschlange, indem wir einen zufälligen Codebereich loslassen, der wahrscheinlich einige Zeit später parallel mit der Finalisierung ausgeführt wird. Lassen Sie uns das Beispiel noch weiter verbessern.


 public class FileWrapper : IDisposable { IntPtr _handle; bool _disposed; public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Dispose() { if(_disposed) return; _disposed = true; InternalDispose(); GC.SuppressFinalize(this); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } private void InternalDispose() { CloseHandle(_handle); } ~FileWrapper() { InternalDispose(); } /// other methods } 

Unser Beispiel für einen Typ, der eine nicht verwaltete Ressource kapselt, sieht nun vollständig aus. Leider ist das zweite Dispose() tatsächlich ein Standard der Plattform und wir erlauben es, es aufzurufen. Beachten Sie, dass Benutzer häufig den zweiten Aufruf von Dispose() zulassen, um Probleme mit einem aufrufenden Code zu vermeiden, und dies ist falsch. Ein Benutzer Ihrer Bibliothek, der sich die MS-Dokumentation ansieht, glaubt dies möglicherweise nicht und lässt mehrere Aufrufe von Dispose () zu. Das Aufrufen anderer öffentlicher Methoden zerstört ohnehin die Integrität eines Objekts. Wenn wir das Objekt zerstört haben, können wir nicht mehr damit arbeiten. Dies bedeutet, dass wir CheckDisposed zu Beginn jeder öffentlichen Methode aufrufen müssen.


Dieser Code enthält jedoch ein schwerwiegendes Problem, das verhindert, dass er wie beabsichtigt funktioniert. Wenn wir uns daran erinnern, wie die Speicherbereinigung funktioniert, werden wir eine Funktion bemerken. Beim Sammeln von Müll finalisiert GC in erster Linie alles, was direkt von Object geerbt wurde. Als nächstes werden Objekte behandelt, die CriticalFinalizerObject implementieren. Dies wird zu einem Problem, da beide von uns entworfenen Klassen Object erben. Wir wissen nicht, in welcher Reihenfolge sie zur „letzten Meile“ kommen werden. Ein übergeordnetes Objekt kann jedoch seinen Finalizer verwenden, um ein Objekt mit einer nicht verwalteten Ressource abzuschließen. Das klingt allerdings nicht nach einer großartigen Idee. Die Reihenfolge der Fertigstellung wäre hier sehr hilfreich. Um dies festzulegen, muss der untergeordnete Typ mit einer gekapselten nicht verwalteten Ressource von CriticalFinalizerObject geerbt werden.


Der zweite Grund ist tiefer. Stellen Sie sich vor, Sie hätten es gewagt, eine Anwendung zu schreiben, die sich nicht viel um das Gedächtnis kümmert. Es reserviert Speicher in großen Mengen, ohne Einlösung und andere Feinheiten. Eines Tages wird diese Anwendung mit OutOfMemoryException abstürzen. Wenn es auftritt, wird Code speziell ausgeführt. Es kann nichts zugeordnet werden, da dies zu einer wiederholten Ausnahme führt, selbst wenn die erste abgefangen wird. Dies bedeutet nicht, dass wir keine neuen Instanzen von Objekten erstellen sollten. Selbst ein einfacher Methodenaufruf kann diese Ausnahme auslösen, z. B. die der Finalisierung. Ich erinnere Sie daran, dass Methoden kompiliert werden, wenn Sie sie zum ersten Mal aufrufen. Dies ist übliches Verhalten. Wie können wir dieses Problem verhindern? Ganz einfach. Wenn Ihr Objekt von CriticalFinalizerObject geerbt wird, werden alle Methoden dieses Typs sofort beim Laden in den Speicher kompiliert. Wenn Sie Methoden mit dem Attribut [PrePrepareMethod] markieren, werden diese außerdem vorkompiliert und können in Situationen mit geringen Ressourcen sicher aufgerufen werden.


Warum ist das wichtig? Warum zu viel Mühe auf diejenigen geben, die sterben? Weil nicht verwaltete Ressourcen für lange Zeit in einem System ausgesetzt werden können. Auch nach dem Neustart eines Computers. Wenn ein Benutzer eine Datei über eine Dateifreigabe in Ihrer Anwendung öffnet, wird diese von einem Remote-Host gesperrt und beim Timeout oder beim Freigeben einer Ressource durch Schließen der Datei freigegeben. Wenn Ihre Anwendung beim Öffnen der Datei abstürzt, wird sie auch nach dem Neustart nicht freigegeben. Sie müssen lange warten, bis der Remote-Host es freigibt. Außerdem sollten Sie in Finalisierern keine Ausnahmen zulassen. Dies führt zu einem beschleunigten Absturz der CLR und einer Anwendung, da Sie den Aufruf eines Finalizers nicht in try ... catch verpacken können. Ich meine, wenn Sie versuchen, eine Ressource freizugeben, müssen Sie sicher sein, dass sie freigegeben werden kann. Die letzte, aber nicht weniger wichtige Tatsache: Wenn die CLR eine Domäne abnormal entlädt, werden auch die von CriticalFinalizerObject abgeleiteten Finalisierer von Typen aufgerufen, im Gegensatz zu denen, die direkt von Object geerbt wurden.


Dieser Charper wurde von professionellen Übersetzern aus dem Russischen wie aus der Sprache des Autors übersetzt . Sie können uns bei der Erstellung einer übersetzten Version dieses Textes in eine andere Sprache, einschließlich Chinesisch oder Deutsch, unter Verwendung der russischen und englischen Textversion als Quelle helfen.

Wenn Sie "Danke" sagen möchten, können Sie uns am besten einen Stern auf Github oder Forking Repository geben https://github.com/sidristij/dotnetbook

Source: https://habr.com/ru/post/de443958/


All Articles