📜 ⬆️ ⬇️

[DotNetBook] IDisposable implementation: proper use

With this article, I begin to publish a series of articles, the result of which will be a book on the work of the .NET CLR, and .NET as a whole. The IDisposable theme was chosen as an overclocking test. The whole book will be available on GitHub: DotNetBook . So Issues and Pull Requests are welcome :)

Disposing (Disposable Design Principle)



Now, probably, almost any programmer who develops on the .NET platform will say that there is nothing simpler than this pattern. What it is known from the famous templates that are applied on the platform. However, even in the simplest and most famous problem area there is always a second bottom, and behind it there are a number of hidden pockets that you have never looked into. However, both for those who are watching the topic for the first time, and for all the others (just so that each of you remembers the basics (do not skip these paragraphs (I follow!))) - we will describe everything from the very beginning to the very end.
')

IDisposable



If you ask what is IDisposable, you will surely answer that this

public interface IDisposable { void Dispose(); } 


What is the interface for? After all, if we have a smart Garbage Collector, which cleans all memory for us, makes sure that we don’t even think about how to clean memory, it becomes not quite clear why it should be cleaned at all. However, there are nuances.

Note


The chapter published on Habré is not updated and it is possible that it is already somewhat outdated. So, please ask for a more recent text to the original:



There is some misconception that IDisposable is made to free unmanaged resources. And this is only part of the truth. In order to understand at once that this is not the case, it suffices to recall examples of unmanaged resources. Is the File class unmanaged? Not. Maybe DbContext ? Again - no. An unmanaged resource is something that is not part of the .NET type system. That which was not created by the platform, and is outside of its scopa. A simple example is a handle to an open file in the operating system. A handle is a number that uniquely identifies a file opened by the operating system. Not by you, but by the operating system. Those. all control structures (such as file system coordinates on the file system, its fragments in the case of fragmentation and other service information, cylinder numbers, heads, sectors in the case of magnetic HDD) are not inside the .NET platform, but inside the OS. And the only unmanaged resource that goes into the .NET platform is the IntPtr-number. This number in turn turns around FileSafeHandle, which in turn turns around the File class. Those. File class itself is not an unmanaged resource, but it accumulates an unmanaged resource in itself through an additional layer — the open file descriptor IntPtr. How do you read from such a file? Through a series of methods WinAPI or OS Linux.

The second example of unmanaged resources are synchronization primitives in multithreaded and multiprocess programs. Such as mutexes, semaphores. Or the data arrays that are transmitted via p / invoke.

Good. With unmanaged resources sorted out. Why IDisposable in these cases? Because the .NET Framework has no idea about what is happening where it does not exist. If you open a file using OS functions, .NET will not know anything about this. If you allocate a section of memory for your own needs (for example, with the help of VirtualAlloc), .NET also does not know anything about it. And if he doesn’t know anything about it, he will not free the memory that was busy calling VirtualAlloc. Or it will not close the file opened directly through the OS API call. The consequences of this can be completely different and unpredictable. You can get OutOfMemory if you add too much memory and you do not free it (for example, you simply reset the pointer for old memory) or block the file on the file ball for a long time if it was opened through the OS, but was not closed . The example with file balls is especially good, because the lock will remain even after the application is closed: the openness of the file is controlled by the side on which it is located. And the remote side will not receive the signal to close the file if you have not closed it yourself.

In all these cases, a universal and recognizable interaction protocol between the type system and the programmer is needed, which will unambiguously identify those types that require forced closure. This _protocol_ is the IDisposable interface. And it sounds like this: if the type contains an implementation of the IDisposable interface, then after you finish working with its instance, you must call Dispose() .

And for this reason, there are two standard ways to call it. After all, as a rule, you either create an instance of an entity in order to work with it quickly within the framework of one method, or within the lifetime of an instance of your entity.

The first option is when you wrap an instance in using(...){ ... } . Those. You expressly indicate that at the end of the block using the object must be destroyed. Those. must be called Dispose() . The second option is to destroy it at the end of the lifetime of the object, which contains a link to the one that must be released. But after all, in .NET, apart from the finalization method, there is nothing that would hint at the automatic destruction of an object. Right? But the finalization does not suit us at all for the reason that it will be unknown when called. And we need to release exactly when it is necessary: ​​right after we no longer need, for example, an open file. That is why we also need to implement IDisposable on ourselves and in the Dispose method, call Dispose on everyone we owned in order to release them too. Thus, we observe the _protocol_ and this is very important. After all, if someone began to observe a certain protocol, all participants in the process must observe it: otherwise there will be problems.

IDisposable implementation variations



Let's go in implementations of IDisposable from simple to complex.

The first and simplest implementation that can come to mind is simply to take and implement IDisposable:

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


Those. for starters, we create an instance of some resource that needs to be freed and in the Dispose () method it is freed.
The only thing that is not here and what makes the implementation non-consistent is the possibility of further working with an instance of the class after its destruction with the Dispose() method:

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


The call CheckDisposed() must be called the first expression in all public methods of the class. However, if for destroying a managed resource, which is a DisposableResource , the resulting structure of the ResourceHolder class looks fine, then for the case of encapsulating an unmanaged resource, it is not.

Let's think up a variant with an uncontrollable resource.

 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); } 


So what's the difference in the behavior of the last two examples? In the first variant, we describe the interaction of a managed resource with another managed one. This means that if the program works correctly, the resource will be released anyway. After all, we have DisposableResource - managed, which means that the .NET CLR knows it perfectly well and, in the case of incorrect behavior, will free up memory from under it. Notice that I intentionally make no assumption that the type of DisposableResource encapsulates. There can be any kind of logic and structure. It can contain both managed and unmanaged resources. We should not care . We are not asked to decompile other people's libraries every time and see which types they use: managed or unmanaged resources. And if our type uses an unmanaged resource, we cannot but know. We do this in the FileWrapper class. So what happens in this case?

If we use unmanaged resources, it turns out that we again have two options: when everything is fine and Dispose method volunteered (then everything is fine :)) and when something happened and Dispose method could not work. Immediately make a reservation why this may not happen:


In all such cases, there will be a situation of uncontrolled resources suspended in the air. After all, Garbage Collector has no idea what they need to collect. The maximum that he will do - during the next pass he will understand that the graph of the objects containing our FileWrapper object is lost, the last link is lost and the memory will be erased by those objects to which there are links.

How to protect against this? For these cases, we must implement the object finalizer. It is not by chance that the finalizer has this name. This is not a destructor at all, as it may initially seem because of the similarity of the declaration of finalizers in C # and destructors in C ++. The finalizer, unlike the destructor, will invoke * guaranteed *, whereas the destructor may not be called (exactly like Dispose() ). The finalizer is called when the Garbage Collection is launched (so far this knowledge is enough, but in fact everything is somewhat more complicated), and is intended to guarantee the release of the captured resources if something went wrong . And for the case of the release of unmanaged resources, we are obliged to implement the finalizer. Also, I repeat, due to the fact that the finalizer is called when the GC starts, in general, you have no idea when this will happen.

Let's expand our code:

 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 } 


We strengthened the example of knowledge about the process of finalization and thus secured the application from losing information about resources, if something went wrong and Dispose () would not be called. Additionally, we made a call to GC.SuppressFinalize in order to disable the finalization of an instance of a type if Dispose () was called for it. We do not need to release the same resource twice? It is also worth doing for another reason: we remove the load from the queue for finalization, speeding up a random piece of code, in parallel with which finalization will work in a random future.

Now let us further strengthen our example:

 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 } 


Now our example implementation of a type that encapsulates an unmanaged resource looks complete. Re- Dispose() , unfortunately, is the de facto standard of the platform and we allow it to be called. I note that often people allow Dispose() to be called again in order to avoid a hassle with the calling code, and this is not correct. However, a user of your library with an eye on MS documentation may not consider this and allow multiple calls to Dispose() . Calling other public methods in any case breaks the integrity of the object. If we destroyed an object, then it is no longer possible to work with it. This in turn means that we are obliged to insert a CheckDisposed call at the beginning of each public method.

However, in this code there is a very serious problem that will prevent it from working as we had in mind. If we remember how the garbage collection process works, then we notice one detail. When garbage collection GC in the first place finalizes everything that is directly inherited from Object , and then is taken for those objects that implement the CriticalFinalizerObject . In our case, it turns out that both the classes we designed inherit Object: and this is a problem. We have no idea in what order we will go to the "last mile." However, a higher-level object may try to work with an object that stores an unmanaged resource — in its finalizer (although this already sounds like a bad idea). Here the order of finalization would be very useful to us. And in order to set it - we must inherit our type, which encapsulates an unmanaged resource, from CriticalFinalizerObject.

The second reason has deeper roots. Imagine that you allowed yourself to write an application that does not care much about memory. Allotsiruet in large quantities without caching and other intricacies. Once such an application fails with an OutOfMemoryException. And when the application crashes with this exception, special conditions for the execution of the code arise: it cannot try to allocate anything. After all, this will lead to re-exclusion, even if the previous one was caught. This does not mean that we should not create new instances of objects. A simple method call can cause this exception. For example, a call to the finalization method. Recall that methods are compiled when they are called for the first time. And this is the usual behavior. How to protect yourself from this problem? Easy enough. If you inherit an object from CriticalFinalizerObject, then all methods of this type will be compiled immediately when loading the type into memory. Moreover, if you mark methods with the [PrePrepareMethod] attribute, they will also be precompiled and will be safe from the point of view of the call if there are not enough resources.

Why is this so important? Why spend so much effort on those who go into another world? And the thing is that unmanaged resources can hang in the system for a very long time. Even after your application has finished. Even after restarting the computer (if the user opens a file with file balls in your application, it will be blocked by the remote host and released either by timeout or when you release the resource by closing the file. If your application crashes during the open file, it will not be closed even after a reboot. You have to wait long enough for the remote host to release it). Plus, you should not allow throwing exceptions in finalizers - this will lead to an accelerated death of the CLR and the final release from the application: the finalizer calls do not turn into try .. catch . Those. when freeing a resource, you need to be sure that it can still be released. And the last no less interesting fact is that if the CLR performs emergency unloading of the domain, the finalizers of types derived from CriticalFinalizerObject will also be called, unlike those who inherit directly from Object.

SafeHandle / CriticalHandle / SafeBuffer / derivatives



I have some feeling that I’ll open a Pandora’s box for you now. Let's talk about special types: SafeHandle, CriticalHandle and their derivatives. And finally, we finish our type template, which provides access to the unmanaged resource. But before that, let's try to list everything that comes to us from the unmanaged world:



SafeHandle is a special .NET CLR class that inherits CriticalFinalizerObject and is designed to wrap operating system descriptors as safely and conveniently as possible.

 [SecurityCritical, SecurityPermission(SecurityAction.InheritanceDemand, UnmanagedCode=true)] public abstract class SafeHandle : CriticalFinalizerObject, IDisposable { protected IntPtr handle; // ,    private int _state; //  (,  ) private bool _ownsHandle; //    handle.   ,     handle       private bool _fullyInitialized; //   [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] protected SafeHandle(IntPtr invalidHandleValue, bool ownsHandle) { } //     Dispose(false) [SecuritySafeCritical] ~SafeHandle() { Dispose(false); } //  hanlde    ,     p/invoke Marshal -  [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] protected void SetHandle(IntPtr handle) { this.handle = handle; } //    ,   IntPtr     .  //   ,    ,       //   .  ,      : // -        SetHandleasInvalid, DangerousGetHandle //       . // -        .     // ,       .       // IntPtr   ,             //      IntPtr [ResourceExposure(ResourceScope.None), ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] public IntPtr DangerousGetHandle() { return handle; } //   (    ) public bool IsClosed { [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] get { return (_state & 1) == 1; } } //      .    ,  . public abstract bool IsInvalid { [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] get; } //     Close() [SecurityCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] public void Close() { Dispose(true); } //     Dispose() [SecuritySafeCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] public void Dispose() { Dispose(true); } [SecurityCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] protected virtual void Dispose(bool disposing) { // ... } //       ,  ,  handle    . //     ,    [SecurityCritical, ResourceExposure(ResourceScope.None)] [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] [MethodImplAttribute(MethodImplOptions.InternalCall)] public extern void SetHandleAsInvalid(); //   ,  ,     // .       , ..   //    ,      . //   -     . //     = false,    // SafeHandleCriticalFailure,     SafeHandleCriticalFailure // Managed Debugger Assistant    . [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] protected abstract bool ReleaseHandle(); //    .      [SecurityCritical, ResourceExposure(ResourceScope.None)] [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] [MethodImplAttribute(MethodImplOptions.InternalCall)] public extern void DangerousAddRef(ref bool success); public extern void DangerousRelease(); } 


To evaluate the usefulness of a group of classes derived from SafeHandle, it is enough to remember what all the .NET types are good for: the automation of garbage collection. Thus, wrapping an unmanaged resource, SafeHandle gives it the same properties, because is manageable. Plus, it contains an internal counter for external links that cannot be counted by the CLR. Those.links from unsafe code. There is almost no need to manually increase and decrease the counter: when you declare any type that derives from SafeHandle as an unsafe method parameter, then when entering the method, the counter will be increased, and when you exit, it will be reduced. This property was introduced for the reason that when you went into unsafe code, passing a descriptor there, then in another thread (if you, of course, work with one descriptor from several threads), reset the link to it, you will get the assembled SafeHandle. With the counter of links, everything is simpler: SafeHandle will not be assembled until the counter is additionally reset. That is why manually change the counter is not worth it. Either this must be done very carefully: returning it as soon as it becomes possible.

The second assignment of the reference counter is the task of the order of finalizationCriticalFinalizerObjectthat link to each other. If one SafeHandle-based type refers to another SafeHandle-based type, then in the constructor of the referencing it is necessary to further increase the reference count, and in the ReleaseHandle method - to decrease. Thus, your object will not be destroyed until the one to which you referred is destroyed. However, in order not to be confused, you should avoid such situations.

Let's write the final version of our class, but now with the latest knowledge about SafeHandlers:

 public class FileWrapper : IDisposable { SafeFileHandle _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; _handle.Dispose(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } [DllImport("kernel32.dll", EntryPoint = "CreateFile", SetLastError = true)] private static extern SafeFileHandle CreateFile(String lpFileName, UInt32 dwDesiredAccess, UInt32 dwShareMode, IntPtr lpSecurityAttributes, UInt32 dwCreationDisposition, UInt32 dwFlagsAndAttributes, IntPtr hTemplateFile); /// other methods } 


What makes it different? Knowing that if you set any SafeHandle-based type as the return value in the DllImport method, Marshal will correctly create and initialize it by setting the usage counter to 1, we set the SafeFileHandle type as the return value for the CreateFile kernel function. When we receive it, we will use it when we call ReadFile and WriteFile (because when we call, the counter will increase again, and when it exits, it will decrease, which will give us a guarantee of the existence of a handle for the entire time of reading and writing to the file). This type is designed correctly, which means that it is guaranteed to close the file descriptor. Even when the process crashes. And this means that we do not need to implement our finalizer and everything connected with it. Our type is much simpler.

Multithreading


Now let's talk about thin ice. In the previous parts of the story about IDisposable, we spoke about one very important concept, which lies not only at the basis of the design of Disposable types, but also in the design of any type: the concept of the integrity of an object. This means that at any time an object is in a strictly defined state and any action on it translates its state into one of the predetermined ones - when designing the type of this object. In other words - no action on an object should be able to transfer its state to one that was not defined. This implies a problem in the previously designed types: they are not thread-safe. There is a potential possibility of calling public methods of these types while the object is being destroyed. Let's solve this problem and decide whether to solve it at all.

 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 } 


Installation of the critical section on the verification code _disposedin Dispose () and in fact

The second, and in my opinion, the most important. We assume a situation of simultaneous destruction of an object with the opportunity to work with it once more. What should we hope for in this case? What does not shoot? After all, if Dispose works first, then further handling of the object's methods must result in ObjectDisposedException. This leads to a simple conclusion: the synchronization between calls to Dispose () and other public methods of the type must be delegated to the serving party. Those.the code that created the class instance FileWrapper. After all, only the creating side is aware of what it is going to do with the class instance and when it is going to destroy it.

Two Levels Disposable Design Principle


What is the most popular implementation pattern IDisposablefound in books on .NET development and on the World Wide Web? What kind of template do people in companies expect from you when you go to interview for a potentially new job? Most likely this:

 public class Disposable : IDisposable { bool _disposed; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if(disposing) { //    } //    } protected void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } ~Disposable() { Dispose(false); } } 


What is wrong here and why have we never written like that earlier in this book? In fact, the template is good and without unnecessary words covers all life situations. But its use everywhere, in my opinion, is not the rule of good tone: after all, we practically never see real unmanaged resources in practice, and in this case, the half-template works as idle. Moreover, it violates the principle of sharing responsibility. After all, it simultaneously manages both managed resources and unmanaged ones. In my humble opinion, this is completely wrong. Let's look at a slightly different approach. * Disposable Design Principle *. In short, the essence is as follows:

Disposing is divided into two levels of classes:


That is why from the very beginning I introduced division into two types: into a containing managed resource and containing an unmanaged resource. They have to work completely differently.

Results


pros


So, we have learned a lot about this simplest pattern. Let's define its advantages:
  1. The main advantage of the template is the possibility of deterministic release of resources: when necessary
  2. Introducing a well-known way to find out that a particular type requires the destruction of its instances at the end of its use.
  3. With proper implementation of the template, the work of the designed type will become safe from the point of view of using third-party components, as well as from the point of view of unloading and destruction of resources during the process collapse (for example, due to lack of memory)


Minuses


I see a lot more disadvantages of the template than advantages:
  1. , , , , : , . , , . , , IDE ( , Dis… , ). Dispose , . , . :
     IEnumerator<T> 
    IDisposable ?
  2. , , IDisposable : IDisposable. «» , . , . , * -*, . Dispose() — . * *. — , ;
  3. , Dispose() . **. . , CheckDisposed() . , : « !»;
  4. , IDisposable *explicit* . , IDisposable , . , . Dispose(), ;
  5. . . GC . , , DisposableObject, , virtual void Dispose() , , ;
  6. Dispose() , tor . disposing .
  7. , , , — ** . , Dispose() . . , Lifetime.


Grand total


  1. IDisposable . , , ;
  2. IDisposable . , , Garbage Collector;
  3. IDisposable Dispose() . : , IDisposable ;
  4. . Those. , : , SafeHandle / CriticalHandle / CriticalFinalizerObject. Dispose(): .
  5. , . , Inversion of Control Lifetime , .


Link to the whole book



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


All Articles