Resource Cleanup


Garbage collection is a key responsibility of the runtime. It is important to note, however, that the garbage collection relates to memory utilization. It is not about the cleaning up of file handles, database connection strings, ports, or other limited resources.

Finalizers

Finalizers allow programmers to write code that will clean up a class's resources. However, unlike constructors that are called explicitly using the new operator, finalizers cannot be called explicitly from within the code. There is no new equivalent such as a delete operator. Rather, the garbage collector is responsible for calling a finalizer on an object instance. Therefore, developers cannot determine at compile time exactly when the finalizer will execute. All they know is that the finalizer will run sometime between when an object was last used and before the application shuts down. (Finalizers will execute barring process termination prior to the natural closure of the process. For instance, events such as the computer being turned off or a forced termination of the process will prevent the finalizer from running.)

The finalizer declaration is identical to the destructor syntax of C#'s predecessornamely, C++. As shown in Listing 9.20, the finalizer declaration is prefixed with a tilde before the name of the class.

Listing 9.20. Defining a Finalizer

 class TemporaryFileStream {  public TemporaryFileStream()  {      Stream = new FileStream(File.FullName, FileMode.OpenOrCreate, FileAccess.ReadWrite);  }     //Finalizer                                                    ~TemporaryFileStream()                                           {                                                                    Close();                                                    }                                                               public FileStream Stream  {      get { return _Stream; }         private set { _Stream = value; }  }  private FileStream _Stream;  public FileInfo File  {      get { return _File; }          private set { _File = value; }  }  private FileInfo _File =      new FileInfo(Path.GetTempFileName());  public void Close()  {      _Stream.Close();      File.Delete();  } }

Finalizers do not allow any parameters to be passed, and as a result, finalizers cannot be overloaded. Furthermore, finalizers cannot be called explicitly. Only the garbage collector can invoke a finalizer. Therefore, access modifiers on finalizers are meaningless, and as such, they are not supported. Finalizers in base classes will be invoked automatically as part of an object finalization call.

Because the garbage collector handles all memory management, finalizers are not responsible for de-allocating memory. Rather, they are responsible for freeing up resources such as database connections and file handles, resources that require an explicit activity that the garbage collector doesn't know about.

Language Contrast: C++Deterministic Destruction

Although finalizers are similar to destructors in C++, the fact that their execution cannot be determined at compile time makes them distinctly different. The garbage collector calls C# finalizers sometime after they were last used, but before the program shuts down; C++ destructors are automatically called when the object (not a pointer) goes out of scope.

Although running the garbage collector can be a relatively expensive process, the fact that garbage collection is intelligent enough to delay running until process utilization is somewhat reduced offers an advantage over deterministic destructors. Deterministic destructors will run at compile-time-defined locations, even when a processor is in high demand and the overall memory consuption is low.


Deterministic Finalization with the using Statement

The problem with finalizers on their own is that they don't support deterministic finalization (the ability to know when a finalizer will run). Rather, finalizers serve the important role of a backup mechanism for cleaning up resources if a developer using a class neglects to call the requisite cleanup code explicitly.

For example, consider the TemporaryFileStream that not only includes a finalizer but also a Close() method. The class uses a file resource that could potentially consume a significant amount of disk space. The developer using TemporaryFileStream can explicitly call Close() in order to restore the disk space.

Providing a method for deterministic finalization is important because it eliminates a dependency on the indeterminate timing behavior of the finalizer. Even if the developer fails to call Close() explicitly, the finalizer will take care of the call. The finalizer will run later than if it was called explicitly, but it will be called.

Because of the importance of deterministic finalization, the base class library includes a specific interface for the pattern and C# integrates the pattern into the language. The IDisposable interface defines the details of the pattern with a single method called Dispose(), which developers call on a resource class to "dispose" of the consumed resources. Listing 9.21 demonstrates the IDisposable interface and some code for calling it.

Listing 9.21. Resource Cleanup with IDisposable

[View full width]

class TemporaryFileStream : IDisposable {      public TemporaryFileStream()      {           _Stream = new FileStream(File.FullName, FileMode.OpenOrCreate, FileAccess. ReadWrite);      }      ~TemporaryFileStream()      {           Close();      }      public FileStream Stream      {           get { return _Stream; }           private set { _Stream = value; }      }      private FileStream _Stream;      public FileInfo File      {           get { return _File; }           private set { _File = value; }      }      private FileInfo _File = new FileInfo(Path.GetTempFileName());      public void Close()      {           _Stream.Close();           File.Delete();           // Turn off calling the finalizer System.GC.SuppressFinalize(this);  } #region IDisposable Members                                      public void Dispose()                                              {                                                                    Close();                                                     }                                                                #endregion                                                                                                      } ___________________________________________________________________________ ___________________________________________________________________________ class Program {  // ...  static void Search()  {        TemporaryFileStream fileStream =              new TemporaryFileStream();        //Use temporary file stream;        // ...         fileStream.Dispose();                                    // ...  } }

The steps for both implementing and calling the IDisposable interface are relatively simple. However, there are a couple of points you should not forget. First, there is a chance that an exception will occur between the time TemporaryFileStream is instantiated and Dispose() is called. If this happens, Dispose() will not be invoked and the resource cleanup will have to rely on the finalizer. To avoid this, callers need to implement a try/finallyblock. Instead of coding such a block explicitly, C# provides a using statement expressly for the purpose. The resulting code appears in Listing 9.22.

Listing 9.22. Invoking the using Statement

 class Program {    // ...    static void Search()      {           using (TemporaryFileStream fileStream1 =                                     new TemporaryFileStream(),                                               fileStream2 = new TemporaryFileStream())                             {                                                                                    // Use temporary file stream;           }      }    }

The resulting CIL code is identical to the code that would be created if there was an explicit try/finally block, where fileStream.Dispose() is called in the finally block. The using statement, however, provides a syntax shortcut for the try/finally block.

Within a using statement, you can instantiate more than one variable by separating each variable with a comma. The key is that all variables are of the same type and that they implement IDisposable. To enforce the use of the same type, the data type is specified only once rather than before each variable declaration.

Advanced Topic: The using Statement Prior to C# 2.0

Prior to C# 2.0, you could code a using statement with any type that implemented a Dispose() method, regardless of whether the type actually implemented the IDisposable interface. In C# 2.0, however, the using statement requires implementation of the IDisposable interface.


Garbage Collection and Finalization

The IDisposable pattern contains one additional important call. Back in Listing 9.21, the Close() method included a call to System.GC.SuppressFinalize() (captured again in Listing 9.23). Its purpose was to remove the TemporaryFileStream class instance from the finalization (f-reachable) queue.

Listing 9.23. Suppressing Finalization

// ... public void Close()  {     _Stream.Close();     File.Delete();  // Turn off calling the finalizer      System.GC.SuppressFinalize(this);                           }  // ...

The f-reachable queue is a list of all objects that are ready for garbage collection and that also have finalization implementations. The runtime cannot garbage collect objects with finalizers until after their finalization methods have been called. However, garbage collection itself does not call the finalization method. Rather, references to finalization objects are added to the f-reachable queue, thereby ironically delaying garbage collection. This is because the f-reachable queue is a list of "references," and as such, the objects are not garbage until after their finalization methods are called and the object references are removed from the f-reachable queue.

Advanced Topic: Resurrecting Objects

By the time an object's finalization method is called, all references to the object have disappeared and the only step before garbage collection is running the finalization code. However, it is possible to add a reference inadvertently for a finalization object back into the root reference's graph. In so doing, the rereferenced object is no longer inaccessible, and therefore, it is not ready for garbage collection. However, if the finalization method for the object has already run, it will not necessarily be run again unless it is explicitly marked for finalization (using the GC.ReRegisterFinalize() method).

Obviously, resurrecting objects like this is peculiar behavior and you should generally avoid it. Finalization code should be simple and should focus on cleaning up only the resources that it references.


Resource Utilization and Finalization Guidelines

When defining classes that manage resources, you should consider the following.

  1. Only implement finalize on objects with resources that are scarce or expensive. Finalization delays garbage collection.

  2. Objects with finalizers should implement IDisposable to support deterministic finalization.

  3. Finalization methods generally invoke the same code called by IDisposable, perhaps simply calling the Dispose() method.

  4. Deterministic finalization methods like Dispose() and Close() should call System.GC.SuppressFinalize() so that garbage collection occurs sooner and resource cleanup is not repeated.

  5. Code that handles resource cleanup may be invoked multiple times and should therefore be reentrant. (For example, it should be possible to call Close() multiple times.)

  6. Resource cleanup methods should be simple and should focus on cleaning up resources referenced by the finalization instance only. They should not reference other objects.

  7. If a base class implements Dispose(), then the derived implementation should call the base implementation.

  8. Generally, objects should be coded as unusable after Dispose() is called. After an object has been disposed, methods other than Dispose() (which could potentially be called multiple times) should throw an ObjectDisposedException().




Essential C# 2.0
Essential C# 2.0
ISBN: 0321150775
EAN: 2147483647
Year: 2007
Pages: 185

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net