本文共 71231 字,大约阅读时间需要 237 分钟。
Alright! Here it is: the revised “Dispose, Finalization, and Resource Management” Design Guideline entry. I mentioned this work previously
As usual,
Update: 4/16/05: Fixed a few typos that were bugging me & came up during the internal review of this doc.
The CLR’s garbage collector (GC) does an amazing job at managing memory allocated directly to CLR objects, but was explicitly not designed to deal with unmanaged memory and OS-managed resources. In many cases, objects running inside the CLR need to interact with resources from the unmanaged world. There’s a considerable gap between these two worlds, requiring a bridge to explicitly manage the touch points. The responsibility for building such a bridge lies mostly in the hands of managed API developers.
The primary goal when managing such resources is quite simply to make use of them in the most efficient manner. This is especially important when resources are limited, e.g. when only a limited quantity is available. You should strive to provide your users with the controls needed to acquire and release resources as needed, in addition to ensuring a safety net exists to prevent long-running resource leaks. Thankfully, the .NET Framework comes with an array of abstractions to hide the details of unmanaged resources from most platform developers (e.g. with HWnds, database connections, GDI handles, SafeHandle), but the sheer number of resource types available means that you’ll sometimes need to write code to manage them yourself.
This section documents the recommended pattern for implementing both explicit and implicit resource cleanup. This is often referred to as the “Dispose” or “IDisposable” pattern, and normally involves the IDisposable interface, a Dispose method for explicit cleanup, and in some cases a Finalize method for implicit cleanup. Implementing this pattern correctly—when appropriate—is critical to ensuring proper, timely cleanup of resources, and also to provide users with a deterministic, familiar way of disposing of resources.
Annotation (Krzysztof Cwalina): Many people who hear about the Dispose pattern for the first time complain that the GC isn’t doing its job. They think it should collect resources, and that this is just like having to manage resources as you did in the unmanaged world. The truth is that the GC was never meant to manage resources. It was designed to manage memory and it is excellent in doing just that.
Managed types must occasionally encapsulate control over resources that are not managed by the CLR. In such cases, the smallest possible class, or “wrapper,” should be used to encapsulate these resources. Ideally, this thin wrapper should contain just allocation along with provisions for basic access to and freeing of the resources. Enclosing classes can be used to provide a more natural view over the resource through abstracted APIs, taking care not to expose the internal resource wrapper. Following this pattern helps to mitigate many of the risks and difficulties outlined further in this section.
An explicit Dispose method should always be provided to enable users to free resources owned by an instance deterministically. Implicit cleanup by means of a Finalize method is also required when a class directly owns such a resource, but often the resource wrapping class—such as SafeHandle for example—will take care of this for you. This leaves only the task of creating an explicit cleanup mechanism to the Framework developer. We discuss both implicit and explicit cleanup in the next section.
Implicit Cleanup
Implicit cleanup should always be provided by protecting resources with a SafeHandle. In fact, implementing finalizers by hand is seldom necessary thanks to the introduction of this type in the .NET Framework 2.0. If supplementary finalization semantics are required, you can implement the protected Finalizemethod yourself using the special finalizer syntax in your favorite language (e.g. ~T() in C# and !T() in C++). The runtime will invoke Finalize for you nondeterministically as part of the GC’s finalization process, providing a last chance for your object to ensure resources are released at the end of its lifetime. By nondeterministic, this simply means that the GC will call Finalize at an undefined point in time after there are no longer any live references to your object. Correctly implementing finalizers by hand is a notoriously difficult task—please see details further below should you decide you have to do so.
Explicit Cleanup
In every case where a type owns resources—or owns types which themselves own resources—you should give users the ability to explicitly release these them. Developers will then have the option to initiate the release of resources once the object is no longer in use. This alleviates some of the reliance on the GC for destruction of resources (something which can subtly harm performance), and also provides users a deterministic way to reclaim resources. Moreover, if the external resource is scarce or expensive, as is the case with OS-allocated handles (e.g. file handles), performance can be improved and resource starvation avoided if they are released once no longer needed. Explicit control should always be provided with a Dispose method based on the IDisposable interface (in C++, simply write a destructor, ~T(), for your type T; the compiler will generate the entire underlying Dispose mechanics).
Read below for more information on the general pattern, as it actually involves more than simply writing a Dispose method when creating a non-sealed class.
Annotation (Clemens Szyperski): The only problem is that automatic management of memory and objects makes it difficult to ensure that resources held by objects are released deterministically (that is, early). The reliance on the GC can lead programmers to think that they don't need to worry about this anymore, which is not the case. In fact, any object that implements IDisposable should be mentally tagged with a red flag and should not be allowed to fall off the scene without Dispose having been called. The finalization/safe handle safety net is really not good enough to prevent lousy user experiences - such as a file remaining locked for an unexpectedly long time after "save" and "close" of a document window (but with the app still running). Careful use of AppDomains and their forced unloading (which triggers safe handles) is sometimes a way to deal with this rigorously.
Annotation (Herb Sutter): Finalizers are actually even worse than that. Besides that they run late (which is indeed a serious problem for many kinds of resources), they are also less powerful because they can only perform a subset of the operations allowed in a destructor (e.g., a finalizer cannot reliably use other objects, whereas a destructor can), and even when writing in that subset finalizers are extremely difficult to write correctly. And collecting finalizable objects is expensive: Each finalizable object, and the potentially huge graph of objects reachable from it, is promoted to the next GC generation, which makes it more expense to collect by some large multiple.
Today, on most GC systems including .NET, the right advice is: When you have to do cleanup for your object, you almost always want to provide it in a destructor (Dispose), not in a finalizer. When you do want a finalizer, you want it in addition to a destructor (Dispose), not instead of Dispose.
Annotation (Brian Grunkemeyer): There are two different concepts that are somewhat intertwined around object tear-down. The first is the end of the lifetime of a resource (such as a Win32 file handle), and the second is the end of the lifetime of the object holding the resource (such as an instance of FileStream). Unmanaged C++ provided destructors which ran deterministically when an object left scope, or when the programmer called delete on a pointer to an object.
Now that you understand this, note that the Finalize & Dispose methods serve very different purposes. Unfortunately, they’re surfaced differently in different languages. C# calls a finalizer a destructor, forcing you to use ~T(), whereas Dispose is a normal method. In C++ starting with version 2 of the .NET Framework, Dispose methods are generated from code written using the destructor syntax (~T), whereas the finalizer will be specified using some other syntax like !T(). C# arguably emphasized the wrong aspect of object lifetime (instead of resource lifetime) by giving the special C++ name the wrong meaning. But note that when you see the word destructor or ~T() in any discussion on Dispose() or object lifetime, pay attention to exactly what the author intended. I prefer using “finalizer” and “dispose” to alleviate any language-induced confusion. Note that this pattern also calls for Dispose(void) & Dispose(bool) in places.
Generally speaking, it is considered good design for consumers of a disposable instance to call Dispose when they are done using it. This is simpler in languages like C# which provides a “using” block to automate calling Dispose for local objects, and in languages like C++ which fully automate “using” with stack-based semantics. However, implicit cleanup is still necessary for cases where a user neglects or fails to invoke the explicit release mechanism, essentially transferring responsibility to the runtime to perform the cleanup.
If your class is not sealed and has to perform resource cleanup, you should follow the pattern exactly as it appears below. For sealed classes, this pattern need not be followed, meaning you should simply implement your Finalizer and Dispose with the simple methods (i.e. ~T() (Finalize) and Dispose() in C#). When choosing the latter route, your code should still adhere to the guidelines below regarding implementation of finalization and dispose logic.
This pattern has been designed to ensure reliable, predictable cleanup, to prevent temporary resource leaks (as a result of skipped disposes, for example), and most importantly to provide a standard, unambiguous pattern for compilers and developers to author disposable classes and for programmers consuming disposable instances. The description below also offers guidance on when and why you should implement a finalizer, as not every disposable type needs one. Lastly, versioning classes that require resource cleanup poses some challenges, especially when introducing a new type into an existing class hierarchy. This is also discussed below.
Annotation (Herb Sutter): You really don’t want to write a finalizer if you can help it. Besides problems already noted earlier in this chapter, writing a finalizer on a type makes that type more expensive to useeven if the finalizer is never called. For example, allocating a finalizable object is more expensive because it must also be put on a list of finalizable objects. This cost can’t be avoided, even if the object immediately suppresses finalization during its construction (as when creating a managed object semantically on the stack in C++).
If using C++, simply write the usual destructor (~T()) and the compiler will automatically generate all of the machinery described later in this section. In the rare cases where you do want to write a finalizer (!T()) as well, the recommended way to share code is to put as much of the work into the finalizer as the finalizer is able to handle (e.g., the finalizer cannot reliably touch other objects, so don’t put code in there that needs to use other objects), put the rest in the destructor, and have your destructor call your finalizer explicitly.
þ
þ
þ
Annotation (Brad Abrams):
Or
We opted for the first ordering as it ensures that GC.SuppressFinalize() only gets called if the Dispose operation completes successfully.
Annotation (Jeffrey Richter): I too wrestled back and forth with the order of these calls. Originally, I felt that SuppressFinalize should be called prior to Dispose. My thinking was this: if Dispose throws an exception then, it will throw the same exception when Finalize is called and there is no benefit this and the 2nd exception should be prevented. However, I have since changed my mind and I now agree with this guideline that SuppressFinalize should be called after Finalize. The reason is because Dispose() calls Dispose(true) which may throw but when Finalize is called later Dispose(false) is called and this may be a different code path than before and it would be good if this different code path executed. And, the different code path may not throw the exception.
Annotation (Brian Grunkemeyer): The ordering is important to give your finalization code (in Dispose(false)) a chance to clean up the resource even if some higher level guarantees usually made when disposing the object can’t be made.
þ
Annotation (Jeffrey Richter): The idea here is that Dispose(Boolean) knows whether it is being called to do explicit cleanup (the Boolean is true) versus being called due to a garbage collection (the Boolean is false). This distinction is useful because, when being disposed explicitly, the Dispose(Boolean) method can safely execute code using reference type fields that refer to other objects knowing for sure that these other objects have not been finalized or disposed of yet. When the Boolean is false, the Dispose(Boolean) method should not execute code that refer to reference type fields because those objects may have already been finalized.
Annotation (Joe Duffy): Jeff’s comment might seem to be an overstatement under careful examination. For example, can’t you safely access reference type objects that aren’t finalizable? The answer is yes you can, iff you are certain that it doesn’t rely on finalizable state itself. This
þ
þ
When implementing your finalizer, place all finalization cleanup logic inside the Dispose(bool disposing) method. Your Finalize method should make a single virtual call to Dispose(false) and nothing more. As noted above, any logic not appropriate to execute during finalization should be written so as not to fire if the disposing argument is false. Such restrictions are discussed further below.
ý
Annotation (Joe Duffy): Having multiple finalizers in a class hierarchy which follows this pattern can result in redundant calls to perform cleanup logic. A virtual finalize method that automatically chains to base.Finalize()—precisely what the C# compiler creates by default—will make n virtual calls Dispose(bool), where n is the number of finalizers the hierarchy following this pattern. This happens because Finalize is called virtually which in turn virtually calls Dispose(bool), both of which chain to their base classes. So long as types are written to be resilient to multiple disposes (ignoring unnecessary calls), the only problem this will create is the subtle performance overhead to make the redundant chains of virtual method calls.
Annotation (Herb Sutter): Note that what Joe mentions about C# doesn’t happen in C++, because C++ generates the recommended machinery whereby Dispose(bool) in the only point that chains to the base (disposer or finalizer).
ý
Simple Example w/out Finalize (C#)
For the majority of types implementing the Dispose pattern, you will not need to implement your own Finalize method. This example shows the simple case, for example when using a SafeHandle to take care of the implicit cleanup:
public class SimpleCleanup : IDisposable
{
}
Complex Example w/ Finalize (C#)
Consider this example of the shell of a correct base-type implementation of the more complex pattern. That is, an implementation that has its own Finalizer. This also demonstrates what a class newly introducing cleanup into a class hierarchy should look like:
public class ComplexCleanupBase : IDisposable
{
}
This code snippet shows what a class extending ComplexCleanupBase would do to hook into the Dispose and Finalize lifecycle to ensure correct cleanup behavior:
public class ComplexCleanupExtender : ComplexCleanupBase
{
}
Notice that it does not re-implement Dispose or Finalize as its parent class already does so. The base type implementations will correctly forward to the most derived Dispose(bool) method so that resource cleanup occurs in the correct order.
Example w/ Finalize (C++)
Implementing this in C++ can be accomplished using the new syntax:
public ref class ComplexCleanupBase
{
private:
public:
};
Overriding dispose behavior is done as follows. Notice that the chaining to base class cleanup logic happens automatically:
public ref class ComplexCleanupExtender : ComplexCleanupBase
{
private:
public:
};
Versioning Considerations
If you choose to add this pattern to an existing unsealed class, you might end up unintentionally affecting existing subclasses. For the same reasons changing semantic contracts between a base and derived class can cause subtle breaking changes, introducing the concept of disposability into the base of a class hierarchy where it previously didn’t exist can be problematic. Further, you might introduce new compilation warnings for existing subclasses when adding a new method to a base class. This section briefly summarizes a few things you should carefully analyze as part of making such a decision.
·
·
·
Base has Dispose? à | Base has virtual Dispose(bool)? | Base has Finalize? ß | What to do when deriving? | ||
IDisposable à | Virtual Dispose (bool) | Override Finalize ß | |||
no | no | no | implement public sealed | if not already present in base then create protected, else override | if overridden but not sealed in base then re-override and seal |
no | no | yes | |||
no | yes | no | |||
no | yes | yes | |||
publicly | no | no | reimplement with private sealed | ||
publicly | no | yes | |||
nonpublicly | no | no | |||
nonpublicly | no | yes | |||
publicly | yes | no | if base version is not sealed then override public sealed | ||
publicly | yes | yes | |||
nonpublicly | yes | no | |||
nonpublicly | yes | yes |
Annotation (Herb Sutter): This table is drawn from what the C++ compiler generates automatically when it detects a base class not following the Dispose pattern as described in this section.
If a type is implementing disposability either through the pattern described above or a simple implementation of the IDisposable interface, the following guidelines apply. Note that these pertain to any code that runs during the disposal of an instance—either inside Dispose(), Dispose(bool), or any other methods that might get called during Dispose.
Authoring Disposable Classes
þ
Annotation (Jeffrey Richter): This guideline is very important and should always be followed without exception. Without this guideline, a user of a type can't control the resource properly.
Annotation (Herb Sutter): Languages ought to warn on this case. If you have a finalizer, you want a destructor (Dispose). The only exception is value types, because you can’t have either a destructor or a finalizer (because the CLR makes arbitrarily many bitwise copies, it isn’t possible to write teardown correctly for value types).
þ
Annotation (Brian Grunkemeyer): A Dispose(bool) method may be called multiple times because of resurrection (i.e. someone calling GC.ReRegisterForFinalizatio
Annotation (Herb Sutter): Unfortunately, having Dispose(bool) called multiple times isn’t that strange… it’s what C#’s automatically generated finalizer chaining does to the Dispose(bool) pattern by default, and why when authoring class hierarchies in C# it’s especially important to implement the Dispose(bool) pattern only once in the class hierarchy. See Joe Duffy’s annotation earlier in this chapter for more details.
þ
Annotation (Herb Sutter): In C++, you can just follow the natural idiom of storing any other objects whose lifetime is controlled by your own object as being directly held by value, and the above is done automatically. In this example, you’d hold the TextReader by value:
Here R::~R() will automatically call tr.~TextReader(), following the usual C++ semantics. (More specifically, in the compiler-generated machinery R.Dispose(true) will invoke tr.Dispose().)
þ
public class CyclicClassA : IDisposable
{
}
public class CyclicClassB : IDisposable
{
}
In this example, given an instance of CyclicClassA a and CyclicClassB b, if a.cycle = b andb.cycle = a, transitive disposal would ordinarily cause an infinite loop. In the above example, notice that the object’s state is nulled out first to prevent such a cyclic loop from happening.
ý
void NaiveConsumer()
{
}
If Dispose could raise an exception, further finally block cleanup logic will not execute. To work around this, the user would need to wrap every call to Dispose (within their finally block!) in a try block, which leads to very complex cleanup handlers. If executing a Dispose(bool disposing) method, never throw an exception if disposing is false. Doing so will terminate the process if executing inside a finalizer context.
þ
The following example demonstrates one possible approach for recreation. It is meant to show the concept only, not to convey any specific pattern to follow:
class Recreatable : IDisposable
{
}
þ
Note that one such scenario that would justify deviation is if an object can be opened and closed multiple times without recreating the instance. For example, the .NET Framework uses this pattern with the System.Data.SqlClient.SqlConnection class. You are able to open and close a connection to the database multiple times, but an instance should still be disposed of afterwards. Optionally, you can release resources upon Close and lazily reacquire them in instances where multiple opens are possible.
þ
ý
Working With Disposable Objects
þ
Be careful, however, not to dispose of an object while it is still in use. Unlike finalization, it’s very easily to accidentally clean up resources on an object which is still actively in use.
ý
C# and VB Using Statement, C++ Stack Semantics
The C# and VB languages offer a using statement, and the C++ language offers stack allocation semantics, to make it easier for developer to work with disposable objects by automatically disposing when control leaves a precise scope. This happens regardless of whether this occurs through normal control flow or as a result of an exception.
In C# and VB, using is appropriate for fine-grained scopes, where the life of an object spans an easily defined block of code. For longer spanning lifetimes, such as disposable fields, you will instead have to invoke Dispose directly on an object. In C++, holding fields by value is appropriate for disposable fields that are linked to the lifetime of their enclosing object (e.g., “OtherType t;” instead of “OtherType^ t;” where ^ is an object reference indirection). The fields’ Disposers will be called automatically when the enclosing object is destroyed, regardless of whether this occurs through normal control flow or as a result of an exception.
For example, the following C# code:
void UseDisposableObject()
{
}
which is equivalent to the following C++ code:
void UseDisposableObject()
{
gets expanded to IL which looks very similar to the following C# code:
void UseDisposableObject()
{
}
Notice how the using statement cleans up the code at the call-site quite a bit, making dispose much more attractive from the developer’s point of view. You can also write “stacked” using statements which get translated into the obvious boilerplate try/finally blocks:
using (Resource r1 = new Resource())
using (Resource r2 = new Resource())
{
{
If you choose to implement finalization logic for your type, you should take care to do it in the correct manner. Finalizers are notoriously difficult to implement correctly, primarily because you cannot make certain (normally valid) assumptions about the state of the system around you during their execution. The following guidelines should be taken into careful consideration.
Note that these guidelines apply not just to the Finalize (C# ~T(), C++ !T()) method, but to any code that executes during finalization. In the case of the Dispose pattern defined above, this means logic which executes inside Dispose(bool disposing) when disposing is false.
þ
Finalization increases the cost and duration of your object’s lifetime as each finalizable object must be placed on a special finalizer registration queue when allocated, essentially creating an extra pointer-sized field to refer to your object. Moreover, objects in this queue get walked during GC, processed, and eventually promoted to yet another queue that the GC uses to execute finalizers. Increasing the number of finalizable objects directly correlates to more objects being promoted to higher generations, and an increased amount of time spent by the GC walking queues, moving pointers around, and executing finalizers. Also, by keeping your object’s state around longer, you tend to use memory for a longer period of time, which leads to an increase in working set.
þ
þ
For example, a finalizable object a that has a reference to another finalizable object b cannot reliably use b in a’s finalizer, or vice versa. There is no ordering among finalizers (short of a weak ordering guarantee for critical finalization). Also, objects stored in static variables will get collected at certain points during an appdomain unload or while exiting the process. Accessing a static variable that refers to a finalizable object (or calling a static method that may use values stored in static variables, like any sort of tracing infrastructure that writes to a file) is not safe, though you can use Environment.HasShutdownStarted (in v1.1 and higher) to detect whether your finalizer is running during an AD unload or while exiting the process.
Annotation (Jeffrey Richter): Note that it is OK to touch unboxed value type fields.
ý
þ
As an example of two instances in which a Finalize() method could be run more than once, consider the following: 1) As indicated above, verifiable IL can explicitly invoke the Finalize() method on your object multiple times. 2) Any arbitrary, untrusted caller who has a reference to your object can invoke GC.ReRegisterForFinalize during their own finalization. If your object has already been finalized, this will sign it up for an additional trip through the finalization process.
ý
Annotation (Brian Grunkemeyer): Note that your finalizer may run while instance methods on your type are running as well. If you define a finalizer that closes a resource used by your type, you may need to call GC.KeepAlive(this) at the end of any instance method that doesn’t use the this pointer after doing some operation on that resource. If you can use SafeHandle to encapsulate your resource, you can almost always remove the finalizer from your type, which means you no longer have to worry about this race with your own finalizer.
ý
Precisely when and how and when this can occur differs based on whether you are running unhosted or in a hosted environment. In unhosted scenarios, finalization for an object can be skipped during timed-out process exits, if a finalizer thread is aborted after it has dequeued your object and before it has called Finalize (in which case, the runtime proceeds to the next object in the queue), or if a process is terminated without a managed shutdown (e.g. PInvoke to kernel32!ExitProcess). In hosted scenarios these conditions also apply, but the host can further initiate a rude AppDomain unload, in which case only critical finalizers are given a chance to execute. In SQL Server hosting, for example, normal AppDomain unloads will escalate to a rude unload should a thread or finalizer not respond within an acceptable timeframe.
Even in the absence of one of the rare situations noted above, a finalizable object with a publicly accessible reference could have its finalization suppressed by any arbitrary untrusted caller. Specifically, they can call GC.SuppressFinalize on you and prevent finalization from occurring altogether, including critical finalization. A good mitigation strategy to deal with this is to wrap critical resources in a non-public instance that has a finalizer. So long as you do not leak this to callers, they will not be able to suppress finalization. If you migrate to using SafeHandle in your class and never expose it outside your class, you can guarantee finalization of your resources (with the caveats mentioned above and assuming a correct SafeHandle implementation).
þ
ý
ý
ý
Consider the C# example below, which demonstrates a dynamically dispatched call to Dispose. The author of Base likely expected Base’s Dispose method to be called at some point (i.e. that any subclasses would “chain” upwards with a call to base.Dispose(), as would be enforced in all C++-authored derived classes for example), but in reality Derived’s version of Dispose never does this. This results in base class resources not being freed, and likely memory leaks. If the Derived Dispose() method suppressed finalization, resources that Base owns might not even get freed during finalization!
public class Base : IDisposable
{
}
public class Derived : Base
{
}
A few potential solutions for this problem are to make Dispose non-virtual, inject a private DisposeImpl method that Base’s Dispose method delegates to and that the finalizer calls explicitly. This is a risk in general with the Dispose(bool) pattern outlined at the beginning of this section—if subclasses never chain appropriately, the system can exhibit resource de-allocation problems.
Annotation (Brian Grunkemeyer): Note that with the above code, Derived’s finalizer will run, then call Derived’s Dispose method. The C# compiler also adds a try/finally to every finalizer that calls the base class finalizer, so Derived’s finalizer will call Base’s finalizer, which will virtually call Dispose, running Derived’s finalizer a second time. This redundant call to Dispose also illustrates why a class hierarchy should only have one finalizer.
þ
For example, in the following code, list may be null if the constructor throws an exception before list is assigned to:
public class MyClass
{
}
Consider this fix to the finalizer which avoids an unhandled NullReferenceException from occurring on the finalizer thread (which would end up terminating the program):
~MyClass() //fixed
{
}
Annotation (Jeffrey Richter): If a constructor throws an exception, the CLR will still call the object's Finalize method. So, when your Finalize method is called, the object's fields may not have all been initialized; your Finalize method should be robust enough to handle this.
þ
Annotation (Chris Brumme): I describe the threading environment for finalization at.
StringBuilder is a good example of this. If you use a StringBuilder from multiple threads, you will get garbage text built up in the buffer.
Annotation (Brian Grunkemeyer): The CLR may choose to use multiple finalizer threads in the future, and these could be threadpool threads or even your main thread. We need the freedom to run finalizers on these other threads, so we must impose this rather small additional burden on class authors.
ý
ý
ý
Annotation (Rico Mariani): This isn't especially a different issue than other finalization type issues. If you are recycling an unmanaged resource you may already need a finalizer. The time to recycle yourself is when you are Disposed. If you are getting finalized with any frequency at all you are already in trouble—recycling doesn't put you in significantly more trouble. See Objects: Release or Recycle?
ý
ý
Annotation (Brian Grunkemeyer): Consider the finalizer thread more like a threadpool thread – if you break it by polluting its state, you buy it.
ý
Annotation (Brian Grunkemeyer): Back during the implementation of V1.0, I was debugging a problem where a finalizer didn’t seem to be called. To help figure out if the finalizer was running at all, I added a call to Console.WriteLine inside it, but then my app started blowing up with an unhandled ObjectDisposedException. How could simply adding a call to Console.WriteLine to the finalizer break the app?
The problem turned out to be that the underlying console stream was being finalized before my instance. The lesson I learned was to follow this guideline: only use non-finalizable instance data from your own finalizer. But I also happened to own the code for the Console class, so I special cased Console.WriteLine—now we never close the handles for stdout, stdin, or stderr. This is somewhat useful for printf-style debugging and logging, and turned out later to be required to support multiple appdomains within the same process (i.e. you don’t want arbitrary appdomains closing your process-wide handle for stdout). So bottom line: using Console from your finalizer is actually a safe thing to do, but watch out for everything else.
ý
C# Finalizers
The Finalize method is inherited from System.Object on every class, although only those that redefine it are eligible for finalization. C# has special syntax which makes writing finalizers easier, and in fact prevents you from overriding Finalize as though it were an ordinary method. The following code, for example:
public class Resource
{
}
Gets translated by the C# compiler into the following conceptually equivalent (although illegal) C# snippet:
public class Resource
{
}
Annotation (Joe Duffy): Earlier in the .NET Framework’s lifetime, finalizers were consistently referred to as destructors by C# programmers. As we become smarter over time, we are trying to come to terms with the fact that the Dispose method is really more equivalent to a C++ destructor (deterministic), while the finalizer is something entirely separate (nondeterministic). The fact that C# borrowed the C++ destructor syntax (i.e. ~T()) surely had at least a little to do with the development of this misnomer. Confusing the two has been unhealthy in general for the platform, and as we move forward the clear distinction between resource and object lifetime needs to take firm root in each and every managed software engineer’s head.
Annotation (Jeffrey Richter): It is very unfortunate that the C# team chose to use the tilde syntax to define what is now called a finalizer. Programmers coming from an unmanaged C++ background naturally think that you get deterministic cleanup when using this syntax. I wish the team had chosen a symbol other than tilde; this would have helped developers substantially in learning how the .NET platform is different than the unmanaged architecture.
This section shows a more complete and complex example of the dispose pattern in C#, as described in the above sections.
using System;
using System.Security;
using System.ComponentModel;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
public class ComplexWindow : IDisposable
{
}
// derived class
public class MyComplexWindow : ComplexWindow
{
}
Annotation (Brian Grunkemeyer): Note that the derived MyComplexWindow class above doesn’t strictly need to add in its own disposed Boolean – it could instead check to see whether myComponent was set to null in Dispose(bool) and in all methods that use that type. Dispose(bool) should still chain to its base class in all cases though. The drawback with this approach is it may be more difficult to maintain if you start adding multiple fields to your type.
Using classes derived from SafeHandle allows you to wrap a handle to an unmanaged resource. It provides protection for handle recycling security attacks, critical finalization, and special managed/unmanaged interop marshaling. The expectation is that you will subclass SafeHandle (or a type like SafeHandleZeroOrMinusOne
þ
private SafeMyResourceHandle handle;
If your resource is very light-weight, such as a small unmanaged buffer for example, it is not subject to recycling attacks, and your scenario is very performance sensitive, you might consider avoiding SafeHandle. SafeHandle has many advantages, such as reducing the graph promotion due to Finalization, avoid recycling attacks, and guaranteeing no leaks even in the face of rude AppDomain unloading, but it may be too heavy-weight for very light-weight resources.
Annotation (Brian Grunkemeyer): Note that we publicly expose a few SafeHandle subclasses for commonly used handle types, like SafeFileHandle and SafeWaitHandle in the Microsoft.Win32.SafeHandles namespace. For a discussion of what led us to designing SafeHandle, read .
ý
Handle Collector
HandleCollector tracks unmanaged handles in order to initiate collections in response to specific thresholds (specified during construction) being met. Whenever allocating a resource which is managed by a collector, simply call Add; when freed, call Remove. Once the number of handles surpasses a given threshold, a GC will occur, hopefully cleaning up any excess resources that may be eligible.
Annotation (Jeffrey Richter): Internally, HandleCollector and AddMemoryPressure both call GC.Collect. So what this guideline is trying to say is that there are times when calling GC.Collect is useful but you should really try to avoid it if possible. If you do call GC.Collect, you should have a really good reason to do it and HandleCollector/AddMemoryPressure exist for good reasons: because it is better to force a collection and adversely affect performance than it is for your program to malfunction because it can't create another handle to an unmanaged resource or because there isn’t enough unmanaged memory available to for a necessary allocation.
This snippet demonstrates how GDI handles can be limited to a threshold of between 10 and 50. The lower bound is where collection is suggested to begin, while the upper bound is a hard limit on the absolute number of resources that are available:
HandleCollector collector = new HandleCollector("GdiHandles", 10, 50);
IntPtr CreateSolidBrush()
{
}
void DeleteBrush(IntPtr handle)
{
}
Add and Remove Memory Pressure
Using GC.AddMemoryPressure gives hints to the GC when a managed object’s cost is higher than it appears due to unmanaged resources. Pass to it the size of the unmanaged resource, and the GC will alter its collection strategy in response to the increased amount of pressure on an object. GC.RemoveMemoryPressure is used to reduce the pressure once the resources have been freed. For example, the following snippet of code demonstrates how to add and remove memory pressure each time a new Bitmap is acquired and released respectively:
public class Bitmap : IDisposable
{
}
Note, however, that if you’re allocating a large number of small byte allocations, it is more efficient to add and remove pressure in large chunks. For example, megabytes or 100’s of kilobytes at a time. You might want to implement a special pressure manager class to handle this, as shown in the following snippet. The BitmapPressureManager class adds and removes memory pressure transactions in 500KB quantities of memory. The Bitmap class from above has been modified to call through to this new class instead:
public class Bitmap : IDisposable
{
}
internal static class BitmapPressureManager
{
}
原文链接:
扩展阅读:
<推荐>
StackOverFlow:
转载地址:http://kpkpi.baihongyu.com/