Garbage Collection: Automatic Dynamic Memory Management

   


Whereas classes are written in the source code before a program is executed, their object counterparts are created dynamically during runtime by using the new operator as described earlier. Every instantiated object occupies a certain amount of memory needed to store items, such as the object's instance variables. Because memory is a scarce resource, it is important to reclaim and thereby recycle the memory representing objects that are no longer used in the program to make space for newly instantiated objects coming into use.

How Do Objects Get Out of Reach?

Before we look at how C# performs this reclaim, it is worth looking closer at what we mean by "an object no longer used." Briefly explained, it is an object that can no longer be reached from the program, meaning that no references to it exist anymore. The program in Listing 13.4 contains four different examples of how an object can lose all its references and thereby get out of reach.

Listing 13.4 AccountsReclaiming.cs
01: using System; 02: 03: class Account 04: { 05: 06: } 07: 08: class Bank 09: { 10:     private Account bankAccount; 11: 12:     public void SetAccount (Account newAccount) 13:     { 14:         bankAccount = newAccount; 15:     } 16: } 17:  18: class Tester 19: { 20:     public static void Main() 21:     { 22:         Bank bank1; 23: 24:         Account account1; 25:         Account account2; 26: 27:         account1 = new Account(); 28: 29:         account1 = null; 30: 31:         account1 = new Account(); 32:         account2 = new Account(); 33: 34:         account1 = account2; 35: 36:         bank1 = new Bank(); 37:         bank1.SetAccount(new Account()); 38: 39:         bank1 = null; 40: 41:         DoingAccountStuff(); 42:     } 43: 44:     public static void DoingAccountStuff() 45:     { 46:         Account localAccount; 47:         localAccount = new Account(); 48:     } 49: } 

No output from this listing.

To make the code shorter I have left the Account class (lines 3 6) empty and the Bank class (lines 8 16) with only one Account.

The Main() method of the Tester class demonstrates four ways that an object can get out of reach.

  • Assigning the null reference to all references of an object.

    Line 27 creates an object and assigns its reference to account1. In line 29, the null reference is assigned to account1, which then no longer contains a reference to the original object, as illustrated in Figure 13.2. The original Account object has gone out of reach because its only previous reference is not referencing it anymore.

    Figure 13.2. Assigning the null reference to the only reference of an object.
    graphics/13fig02.gif
  • When a reference variable holding the only reference to an object A is assigned a different reference object, A goes out of reach.

    Lines 31 and 32 assign two different Account objects to account1 and account2. After line 34, both account1 and account2 are referencing the Account object originally referenced by just account2 (see Figure 13.3). As a result, the object originally referenced by account1 has gone out of reach.

    Figure 13.3. Assigning the reference of another object to the only reference of an object.
    graphics/13fig03.gif
  • When an object A contains the only reference to an object B, both object A and B go out of reach when object A goes out of reach.

    In line 36, bank1 is assigned a reference to a new Bank object. This Bank object contains an Account object reference, which is assigned a reference to a new Account object through the call to its SetAccount method in line 37. After line 39, bank1 is no longer referencing the Bank object (see Figure 13.4). Consequently, the Bank object is out of reach. But because the Bank object is out of reach, the Account object it was referencing is also out of reach.

    Figure 13.4. When the Bank object referencing the Account object goes out of reach.
    graphics/13fig04.gif

    If the Account object had referenced yet another object and this object yet another object and so on, a chain of objects connected by references would be formed, all dependent on the references in front of them in the chain to stay within reach.

  • When a local variable containing the only reference to an object goes out of scope, the object goes out of reach.

    The Tester class contains another method apart from Main() called DoingAccountStuff (lines 44 48). When this method is executed, its local Account reference variable called localAccount comes into scope after its declaration in line 46. In line 47, it is assigned a reference to a new Account object. However, localAccount goes out of scope when the method returns to the caller. Thus localAccount is out of reach, and so is the Account object it was referencing.

Note

graphics/common.gif

If you attempt to use a reference variable that does not reference an object, but that instead references null, the runtime will generate a NullReferenceException.


The Tasks of the Garbage Collector

To reclaim the memory of objects that are no longer reachable, C# relies on a mechanism supported by the .NET runtime called a garbage collector (GC).

The runtime's method of automatically managing memory is called garbage collection (GC). The term garbage collection and its associated technique can be traced all the way back to the Lisp language (see Chapter 2, "Your First C# Program").

The GC has two main functions detecting unreachable objects and reclaiming their memory. To perform these important cleanup functions, the GC employs highly complex algorithms that are beyond the scope of this book. However, there are a few aspects you need to be aware of, which are a result of the way these algorithms work:

  • The .NET GC promises not to reclaim any objects that are reachable. For example, the GC will not suddenly gobble up one of the useable Account objects from our previous examples.

  • The .NET GC works in the background of every C# program you run and will eventually and automatically detect and reclaim all objects that are unreachable. Consequently, you don't need to worry about starting it up or shutting it down; in fact, you can just forget it is even there under most circumstances.

  • Notice the highlighted word "eventually" in the previous bullet. Isn't an object reclaimed in the same instant it becomes unreachable? Maybe, but probably not.

  • Let's look a little closer at the circumstances for this likely time lag: Whenever the GC is running to perform its tasks, it occupies scarce processing time on the CPU, which would otherwise be used to execute your program, thus resulting in your program being temporarily frozen. Therefore, the GC must strike a balance between making sure that, on one hand, enough memory is reclaimed to allow new objects to be created and, on the other hand, perform this task by using as little CPU time as possible. If the GC had to fulfill a promise saying: "I will reclaim all objects in the same instant they become unreachable," it would have to occupy a disturbingly large percentage of the CPU's processing time. For that reason, the GC only promises us the following: "I will do what I can to always have enough memory available for your objects while minimizing the time I occupy the CPU. However I cannot promise when I will reclaim the unreachable objects; it could be immediately after they become unreachable, it might not occur until the program is about to close down after it has finished interacting with the end user, or it might occur sometimes in between these two timing extremes."

  • The GC algorithms are based on a technique called the "lazy" technique, that lets the garbage collector work only at irregular intervals and in short spurts. These spurts of activity are often triggered by low memory combined with an immediate need to free up memory for new objects.

Giving a "Run Now" Suggestion to the GC

graphics/common.gif

You might encourage the GC to run by calling the static method Collect of the System.GC class with the following line:

 System.GC.Collect(); 

However, the GC might or might not follow this suggestion. Programmers often call the GC.Collect method after many objects have become unreachable or before parts of the program that are sensitive to being frozen by the activity of the GC are executed. In Listing 13.5, shown later in this chapter, you get a chance to test how responsive the GC is to the GC.Collect call in practice.


GCs and Real Time Applications Do Not Mix Well

graphics/common.gif

When an application controls a series of events that must take place in real time, the application is said to be a real time application. For example, if the program is performing an animation of a cartoon character walking across the screen, each movement (event) of arms, legs, and so on, must happen not only in a certain sequence, but also in a well-timed manner. If the cartoon character makes a jump but then suddenly freezes in midair, the sense of real time has disappeared. A temporarily frozen cartoon character could easily be the result of the GC suddenly interrupting the general execution of the program with a spurt of activity. So, because of our current inability to control the GC, you should think twice before building a real time application based on the .NET GC version available at this time of writing.

Fortunately the GC's algorithms are constantly evolving, so we might soon see a GC with the ability to support real time applications.


Most of the time we don't need to be concerned about the time lag between an object becoming unreachable and the reclaiming of its memory by the GC. However, it is important to keep in mind when we look at different strategies for freeing other scarce resources than memory.

Freeing Scarce Resources Other Than Memory

Sometimes an object occupies scarce resources other than memory. For example, they could be

  • Files An object might, during its existence, need to access a file to read and/or write its contents. This access is usually obtained by letting the object hold a file handle, which is then opened. But only one file exists and, to avoid the chaotic situation of having the same file being changed at the same time by different entities (such as objects), only one object can access the file at any one time. Consequently, it is important that the file handle be closed as soon as the object is not used anymore to allow other objects access.

  • Network/Database connections There are usually only a limited number of network and database connections available that, like files, can be accessed by only one user at a time. It is important that any such connections be freed immediately after the user has finished with them to allow other users access.

How can we ensure that an object promptly frees the scarce resources it is occupying at the moment it becomes unreachable? Before we look at the recommended technique for solving this problem, called the Dispose design pattern, the next section looks at another tempting but incorrect (due to the time lag already described) approach using a destructor.

Note

graphics/common.gif

The garbage collector only reclaims memory. It does not free up any other resources.


Note

graphics/common.gif

Not all object-oriented languages include a garbage collector. In C++, for example, the programmer must manually write the source code that detects when an object is no longer needed and must explicitly call a special method called a destructor (which is semantically different from C#'s destructors) to reclaim the associated memory. This destructor method will execute immediately when called and can include code to terminate access to scarce non-memory resources (files, network connection, and so on) mentioned earlier. Consequently, the C++ programmer has very tight control over when memory and other scarce resources are released by an object.

This control unfortunately comes with a high price. Not only does the C++ programmer spend precious time writing the code needed to implement this detection and reclaim system, but it comes with some of the nastiest and well hidden bugs known in the industry: They cause two main types of problems:

  • Dangling pointers The program mistakenly reclaims an object still in use, leading to a problem known as dangling pointers. (Note: A pointer in C++ is the equivalent to a reference in C#).

  • Memory leaks The program does not detect all objects that should be reclaimed, often causing a program to eventually run out of memory and crash.


The Limited Use of the Destructor

As mentioned in the note preceding this section, C++ contains a language feature called a destructor that is called explicitly in the source code to reclaim the resources occupied by an object. C# contains a vaguely similar construct in the form of a special method also called a destructor. Just like every class (not containing any explicit constructors) is equipped with a default constructor, every C# class is also equipped with a default destructor. However, whereas you must explicitly call a constructor with the new keyword to create a new object, only the GC can call the destructor when a C# program is running. The syntax for specifying the statements you want the destructor to execute is shown in Syntax Box 13.5.

Syntax Box 13.5 The Destructor

 Destructor_declaration::=                         ~ <Destructor_identifier> ( )                         {                             <Statements>                         } 

Example:

 class Account {     ...     ~Account()     {         Console.WriteLine("I'm only called by the GC " +             "and I might never be called during the execution of  graphics/ccc.gifthe program");     }     ... } 

Notes:

  • The <Destructor_identifier> must, like a constructor, be identical to the class identifier.

  • No formal parameters can be specified for the destructor.

  • The following syntax has exactly the same meaning as the destructor declaration shown in the beginning of this Syntax Box:

 protected override void Finalize () {     <Statements> } 

This is because destructors in C# are known as finalizers in the .NET runtime, so both kinds of syntax have found their way into C#.

The GC calls the destructor of an object when the GC happens to detect that the object is unreachable (see the following Note for further details). Superficially, the destructor sounds like the perfect place to put the code used to free scarce non-memory resources, but, due to the time lag previously discussed, the destructor could be called at any point between the time it becomes eligible for reclaim and after the program has ended. A destructor might never be called during the execution of a program. Consequently, the destructor is not suited for freeing up scarce non-memory resources. There are several other reasons why you should think twice before declaring destructors in your classes. They are discussed in the next section.

The Resource Hungry Mechanics Behind Destructors

Here is a brief overview of how the GC works with destructors and highlights some of their inefficiencies.

When collecting garbage, the GC distinguishes between objects containing a destructor declaration explicitly specified by you, as in Syntax Box 13.5 (called objects with destructors) and objects without destructor declarations.

When the GC during a spurt of activity detects an object to be unreachable and without a destructor, it is reclaimed immediately. In contrast, when an unreachable object with a destructor declaration is detected, its reference is put in a special list with other objects of the same fate, all waiting to have the statements of their destructors executed. When the GC has finished its spurt of activity detecting unreachable objects, it starts up another process to execute the statements of each destructor in the objects of the special list. Each object reference is then finally positioned in another list ready to be reclaimed. However, this will not take place until the next time the GC has a spurt of activity.

It is evident from this GC portrayal that handling objects with destructors is a costly process both in terms of memory and CPU time. An object with a destructor needs two full GC spurts to be fully reclaimed, consequently occupying the memory for a longer period of time, and special processes are needed along with space occupied by references in special lists. But these are not the only reasons why you should avoid using destructors. The following are a few other points to take into consideration:

  • The order in which the destructors of the objects are executed is not guaranteed.

  • There is no guarantee as to when or if the destructors will be called.

  • Not only do objects with destructors occupy memory for a longer period, but also might have references to other objects (with or without destructors) and also force those to lengthen their stay.

Putting the Destructor to Good Use

By now, you might wonder if the C# destructor can be used for anything at all. It turns out it has a couple of uses:

  • To release non-scarce resources occupied by an object. For example, this could be a file access to a file that is only accessed by one object.

  • To investigate the process of garbage collection.

We use this second ability in Listing 13.5 to give you a practical view of the GC's activity spurts and their timing during the execution of a program preoccupied with constructing new objects that become unreachable and eligible for garbage collection immediately after their creation.

Listing 13.5 GCAnalyzer.cs
 01: using System;  02:  03: class Bacterium  04: {  05:     private static int bacteriaCreated;  06:     private static int tempBacteriaDestructed;  07:     private static int totalBacteriaDestructed;  08:     private static bool showBacteriaDestructed;  09:     private static bool hasGCJustRun;  10:  11:     private int number;  12:     private string name;  13:     private string shape;  14:     private string growthMethod;  15:     private string group;  16:  17:     public Bacterium ()  18:     {  19:         if (hasGCJustRun)  20:         {  21:             Console.WriteLine("Bacterium objects destructed during GC Run: {0}",  22:                 tempBacteriaDestructed);  23:             Console.WriteLine("Difference between bacteria created " +  24:                 "and bacteria destructed {0}",  25:                 (bacteriaCreated - totalBacteriaDestructed));  26:             Console.WriteLine("Resuming creating bacteria from number: {0}\n",  27:                 bacteriaCreated);  28:             hasGCJustRun = false;  29:         }  30:  31:         bacteriaCreated++;  32:         number = bacteriaCreated;  33:         name = "Streptococcus";  34:         shape = "round";  35:         growthMethod = "chains";  36:         group = "alpha";  37:     }  38:   39:     static Bacterium ()  40:     {  41:         bacteriaCreated = 0;  42:         tempBacteriaDestructed = 0;  43:         totalBacteriaDestructed = 0;  44:         showBacteriaDestructed = false;  45:         hasGCJustRun = false;  46:     }  47:  48:     ~Bacterium()  49:     {  50:         if(!hasGCJustRun)  51:         {  52:             Console.WriteLine("Creation temporarily stopped " +  53:                 "at bacteria number: { 0}  to perform destruction", bacteriaCreated);  54:             hasGCJustRun = true;  55:             tempBacteriaDestructed = 0;  56:         }  57:         if (showBacteriaDestructed)  58:         {  59:             Console.WriteLine("Bacteria: { 0}  destructed", number);  60:         }  61:         tempBacteriaDestructed++;  62:         totalBacteriaDestructed++;  63:     }  64:  65:     public static void SetShowBacteriaDestructed (bool showIt)  66:     {  67:         showBacteriaDestructed = showIt;  68:     }  69:  70:     public static int GetTotalBacteriaDestructed ()  71:     {  72:         return totalBacteriaDestructed;  73:     }  74:  75:     public static int GetTotalBacteriaCreated ()  76:     {  77:         return bacteriaCreated;  78:     }  79: }  80:  81: class Body  82: {  83:     public static void Main()  84:     {  85:         int bacteriaCreatedBeforeCollect;  86:         int maxBacteria;  87:         Bacterium newBacterium;  88:   89:         Console.Write("How many bacteria do you want to create? ");  90:         maxBacteria = Convert.ToInt32(Console.ReadLine());  91:         Console.Write("Enter amount of bacteria to create " +  92:             "before asking GC to run: ");  93:         bacteriaCreatedBeforeCollect = Convert.ToInt32(Console.ReadLine());  94:         Console.Write("Do you want to see each " +  95:             "bacterium number when destructed? Y)es N)o ");  96:         if (Console.ReadLine().ToUpper() == "Y")  97:             Bacterium.SetShowBacteriaDestructed (true);  98:         else  99:             Bacterium.SetShowBacteriaDestructed (false); 100:         Console.WriteLine("\ nCreation commencing\ n"); 101: 102:         for (int i = 0; i < maxBacteria; i++) 103:         { 104:             newBacterium = new Bacterium(); 105:             if (i == (bacteriaCreatedBeforeCollect - 1)) 106:             { 107:                 Console.WriteLine("Initiating GC to run " + 108:                     "after bacteria number: { 0}  has been created", (i + 1)); 109:                 GC.Collect(); 110:             } 111:         } 112: 113:         Console.WriteLine("\ nCreation stopped at bacterium number: { 0} ", 114:             Bacterium.GetTotalBacteriaCreated()); 115:         Console.WriteLine("Total Bacteria objects destructed " + 116:             "during execution of program: { 0} ", 117:             Bacterium.GetTotalBacteriaDestructed()); 118:     } 119: } 

Notes:

  • You will likely get the following four warnings when compiling the source code of Listing 13.5.

     GCAnalyzer.cs(12,20): warning CS0169: The private field 'Bacterium.name' is never used GCAnalyzer.cs(13,20): warning CS0169: The private field 'Bacterium.shape' is never used GCAnalyzer.cs(14,20): warning CS0169: The private field 'Bacterium.growthMethod' is never  graphics/ccc.gifused GCAnalyzer.cs(15,20): warning CS0169: The private field 'Bacterium.group' is never used 

    You can safely ignore these warnings. The four instance variables (name, shape, growthMethod, and group) of the Bacterium class have only been included to let each object occupy more memory and thereby require the creation of fewer Bacterium objects to run low on memory and trigger a GC spurt of activity.

  • The timing and duration of each GC spurt depends on several variables, including the memory size of your computer, other programs you have running that might take up memory, and so on. As a result, the output, concerned with GC timing and duration, will vary between different runs of the program and will certainly not be identical to the following output.

Sample output 1:

 How many bacteria do you want to create? 1000000<enter> Enter amount of bacteria to create before asking GC to run: 1000001<enter> Do you want to see each bacterium number when destructed? Y)es N)o N<enter> Creation commencing Creation temporarily stopped at bacterium number: 15759 to perform destruction Bacteria objects destructed during GC Run: 8701 Difference between bacteria created and bacteria destructed 7058 Resuming creating bacteria from number: 15759 Creation temporarily stopped at bacterium number: 18367 to perform destruction Bacteria objects destructed during GC Run: 9031 Difference between bacteria created and bacteria destructed 635 Resuming creating bacteria from number: 18367 Creation temporarily stopped at bacterium number: 899227 to perform destruction Bacteria objects destructed during GC Run: 87700 Difference between bacteria created and bacteria destructed 4642 Resuming creating bacteria from number: 899227 Creation temporarily stopped at bacterium number: 982952 to perform destruction Bacteria objects destructed during GC Run: 87699 Difference between bacteria created and bacteria destructed 668 Resuming creating bacteria from number: 982952 Creation stopped at bacterium number: 1000000 Total Bacteria objects destructed during execution of program: 982284 

Sample output 2:

 How many bacteria do you want to create? 1000<enter> Enter amount of bacteria to create before asking GC to run: 1001<enter> Do you want to see each bacterium number when destructed? Y)es N)o N<enter> Creation commencing Creation stopped at bacterium number: 1000 Total Bacteria objects destructed during execution of program: 0 

Sample output 3:

 How many bacteria do you want to create? 5000<enter> Enter amount of bacteria to create before asking GC to run: 3000<enter> Do you want to see each bacterium number when destructed? Y)es N)o N<enter> Creation commencing Initiating GC to run after bacterium number: 3000 has been created Creation temporarily stopped at bacterium number: 3000 to perform destruction Bacteria objects destructed during GC Run: 3000 Difference between bacteria created and bacteria destructed 0 Resuming creating bacteria from number: 3000 Creation stopped at bacterium number: 5000 Total Bacteria objects destructed during execution of program: 3000 

At the heart of Listing 13.5 is a loop (see lines 102 111, you can ignore lines 105 110 for now) creating new Bacterium objects in line 104. Each time a new Bacterium object reference is assigned to newBacterium in line 104, the Bacterium object referenced by newBacterium just prior to this assignment becomes unreachable and is pushed into the unknown. So the loop gradually fills the memory up with unreachable objects that sooner or later must be reclaimed by the GC to create space for new Bacterium objects. If we, for example, set maxBacteria to one million (see sample output 1) in line 90, line 104 is repeated one million times and will likely create objects occupying memory many times your computer's memory capacity. Consequently, the GC will have to run many spurts during the course of the one million loops. While the GC is running, the runtime keeps the program frozen. The only lines of your code being executed during these periods are the destructors (lines 48 63) of the unreachable objects eligible for execution.

The runtime switches back and forth between two modes:

  • The main program is running New Bacterium objects are being created in line 104 causing their Bacterium constructor in lines 17 37 to be executed.

  • The GC is running (main program temporarily stopped) Unreachable objects are detected and reclaimed. The Bacterium destructors (lines 48 63) of these objects are executed.

One of the aims of this program is to detect and display when the runtime switches between these two modes. The main ingredient used for this purpose is the bool variable hasGCJustRun declared in line 9. hasGCJustRun is static, so only one hasGCJustRun exists; it belongs to the Bacterium class and is shared by all Bacterium objects. Both the constructor and destructor of any Bacterium object can reach the same hasGCJustRun variable. Initially, hasGCJustRun is set to false in line 45 (in the static constructor of the Bacterium class) and the only place where hasGCJustRun is ever set to true is in the destructor (line 54). As long as no destructors have been called, no constructor will execute lines 20 29 (due to the if statement in line 19).

After a number of objects have been created (and become unreachable) the GC might decide to run (switching from mode 1 to mode 2) and execute a number of destructors. When the first destructor is executed, the condition of line 50 is true so that lines 52 55 will be executed setting hasGCJustRun to true in line 54. The second time the destructor is called, line 50 is false, preventing lines 52 55 from being executed. In fact, as long as no constructor has been executed to set hasGCJustRun back to false, lines 52 55 will not be executed by any of the trailing destructor executions. After the GC spurt is stopped, the main program commences (switching from mode 2 to mode 1). This time, hasGCJustRun is equal to true causing lines 21 28 to be executed, but only the first constructor in this batch of constructor calls will execute those lines.

So briefly, the overall behavior of the program can be described as when the GC switches from mode 1 to mode 2, lines 52 55 are executed once, and when the GC switches back from mode 2 to mode 1, lines 21 28 are executed once.

To keep track of the number of Bacterium objects created, the constructor increments the static variable bacteriaCreated by one every time it is called. This enables lines 52 53 to report the number of bacteria created when every GC spurt commences. Similarly, the destructor keeps track of the total number of bacteria destructed (line 62) over the course of the program and the number of bacteria destructed in any one spurt (line 61). The latter amount is reported by lines 21 and 22 of the constructor, along with the difference between the total number of bacteria created and the total number of bacteria destructed. The difference gives us an idea of how many unreachable objects the GC allows the memory to contain. Keep in mind, though, that objects that have had their destructors called are not reclaimed until the next time the GC runs.

The first sample output involves the creation of one million Bacterium objects. Notice how the GC spurts keeps the number of unreachable objects in check. Also note that many destructors (1000000 982284 = 17716) were never called during the execution of the program.

The second sample output makes it evident that destructors might never be called during the course of a program.

As mentioned earlier, you can attempt to wake up the GC by calling the System.GC.Collect method. Listing 13.5 allows you to test just how responsive the GC is to this call. As demonstrated in sample output 3, you need in the second question to enter at which number-of-bacteria-created you wish the program to make the System.GC.Collect call. The number is stored in bacteriaCreatedBeforeCollect (declared in line 85, assigned in line 93) and checked during each loop in line 105. The actual call is made in line 109. To avoid having the GC.Collect call being made, simply enter a number larger than the total number of bacteria the program will create (maxBacteria). The GC was 100 percent responsive in sample output 3. It started exactly at 3000, created objects as requested, and destructed all of them. However, during the testing of this program, I experienced several instances where the GC either didn't respond at all, or responded by collecting just a few Bacterium objects.

Controlling the Garbage Collector

graphics/common.gif

There are other ways to control the GC than with the GC.Collect method. For more details, please refer to the System.GC class of the .NET Framework Documentation.

From time to time, your program might benefit from using these tools. However, the GC has been constructed and optimized to work behind the scenes and undisturbed. If you interfere with this finely-tuned mechanism by calling the GC.Collect method or any of the other methods described in the .NET Framework Reference, you might compromise the overall performance of the GC and your program.


The program lets you view the total list of all Bacterium objects that have had their destructor executed. This is simply done by printing out the number of each bacterium in a (probably very long) list. By answering Y to the third question after the program is started, you can initiate this option. For space reasons, no sample output has been provided for this option.

Freeing Scarce Non-Memory Resources with the Dispose Method

If we can't use the destructor method to free up scarce non-memory resources what is the alternative? Microsoft encourages programmers to use what they call the "Dispose design pattern." The following is a simplified description of how it works.

Every object occupying scarce non-memory resources must be equipped with a public method called Dispose with statements to free the object's resources. When the user (for example, an object in another part of the program) is finished using the object, the Dispose method must be called. This ensures an immediate return of the resources and avoids the time lag we would otherwise experience had the destructor been entrusted to accomplish the task. Executing the Dispose method renders the object useless; to avoid any later use of this object and to allow the GC to reclaim its memory eventually, all its references must be released immediately after calling Dispose. Microsoft also suggests writing the same reclaiming statements of the Dispose method in the destructor. This allows the GC to free the resources of an object eventually, even if the Dispose method for some reason is never called. However, it poses a small problem unless instructed otherwise, the GC will call the destructor of an object even if its Dispose method has been called previously. This means trouble, because freeing the same resources twice is an error. Fortunately, there is an easy solution to the problem. Let the Dispose method instruct the GC (by calling System.GC.SuppressFinalize) not to call the destructor of its object.

Listing 13.6 provides a simplified example of how the Dispose design pattern can be implemented. It leaves out issues related to inheritance and interfaces but still amply demonstrates the important ideas behind this design pattern.

Note

graphics/common.gif

For a more advanced account of the Dispose design pattern, please refer to "Common Design Patterns" of ".NET Framework Design Guidelines" in the .NET Framework documentation.


The following gives an overview of the main parts of the program. For a more detailed treatment, please see the analysis after the sample output.

The MyFile class (lines 3 64) of Listing 13.6 simulates a file. Like most files, it only allows access to one user at a time. This is accomplished through a virtual key. Only one virtual key exists in the whole program. MyFile initially holds this key. The only way for an object to gain temporary exclusive access to MyFile is by obtaining the key from MyFile through a call to its Open method. If an object asks for the key while it is in the hands of another object, MyFile is unable to fulfill the request and access is denied. An object terminates access to MyFile by calling the MyFile.Close method; the key is then returned to MyFile.

The main task of an object of FileAccessor class (lines 66 128) is simply to print the content of MyFile (contained in line 5). This cannot be accomplished without accessing MyFile, so access to MyFile is already made in the constructor of each FileAccessor object. Consequently, MyFile is a precious resource and FileAccessor is, for that reason, equipped with a Dispose method and a destructor, both containing statements (in accordance with the Dispose design pattern) to release MyFile by returning the key to MyFile.

The Main() method of the TestFileAccess class creates and destructs several FileAccessor objects attempting to access Myfile, and it shows how the Dispose method should and should not be applied.

Listing 13.6 FileAccessSimulation.cs
 01: using System;  02:  03: class MyFile  04: {  05:     private static string fileContent = "Once upon a time a beautiful princess...";  06:     private const int accessKey = 321;  07:     private static int mobileKey = accessKey;  08:  09:     public static int Open (int accessorNumber)  10:     {  11:         if (mobileKey == accessKey)  12:         {  13:             mobileKey = 0;  14:             Console.WriteLine("Access established to accessor number: {0} ",  15:                 accessorNumber);  16:             return accessKey;  17:         }  18:         else  19:         {  20:             Console.WriteLine("File occupied! Access attempt failed");  21:             return 0;  22:         }  23:     }  24:   25:     public static void Close (int returnKey, int accessorNumber)  26:     {  27:         if (accessKey == returnKey)  28:         {  29:             mobileKey = returnKey;  30:             Console.WriteLine("File closed successfully by accessor number: { 0}  ",  31:                 accessorNumber);  32:         }  33:         else  34:         {  35:             Console.WriteLine("Could not close file, invalid return key");  36:         }  37:     }  38:  39:     public static string Read (int readKey)  40:     {  41:         if (readKey == accessKey)  42:         {  43:             return fileContent;  44:         }  45:         else  46:         {  47:             Console.WriteLine("Access denied! Invalid key provided");  48:             return "";  49:         }  50:     }  51:  52:     public static bool IsOccupied ()  53:     {  54:         if (mobileKey == accessKey)  55:             return false;  56:         else  57:             return true;  58:     }  59:   60:     public static void Reset ()  61:     {  62:         mobileKey = accessKey;  63:     }  64: }  65:  66: class FileAccessor  67: {  68:     private bool fileAccessEstablished;  69:     private bool disposed;  70:     private int accessKey;  71:     private int number;  72:     private static int objectCounter = 0;  73:  74:     public FileAccessor ()  75:     {  76:         objectCounter++;  77:         number = objectCounter;  78:         if (!MyFile.IsOccupied())  79:         {  80:             accessKey = MyFile.Open(number);  81:             fileAccessEstablished = true;  82:             disposed = false;  83:         }  84:         else  85:         {  86:             Console.WriteLine("Error! Accessor { 0}  could not access MyFile",  graphics/ccc.gifnumber);  87:             fileAccessEstablished = false;  88:         }  89:     }  90:  91:     private void FreeState ()  92:     {  93:         if (!disposed)  94:         {  95:             MyFile.Close(accessKey, number);  96:             fileAccessEstablished = false;  97:             disposed = true;  98:         }  99:         else 100:         { 101:             Console.WriteLine("Error! Attempt to dispose accessor { 0} more than  graphics/ccc.gifonce", 102:                 number); 103:         } 104:     } 105: 106:     public void Dispose () 107:     { 108:         FreeState (); 109:         GC.SuppressFinalize(this); 110:     } 111: 112:     public void PrintFileContent () 113:     { 114:         if (!disposed) 115:         { 116:             if (fileAccessEstablished) 117:                 Console.WriteLine("Content printed by accessor { 0} : { 1} ", 118:                     number, MyFile.Read(accessKey)); 119:             else 120:                 Console.WriteLine("Accessor { 0}  has no access to MyFile", number); 121:         } 122:         else 123:         { 124:             Console.WriteLine("Access impossible. Accessor { 0}  has already been  graphics/ccc.gifdisposed", 125:                 number); 126:         } 127:     } 128: } 129: 130: class TestFileAccess 131: { 132:     public static void Main() 133:     { 134:         FileAccessor accessor1; 135:         FileAccessor accessor2; 136:         FileAccessor accessor3; 137:         FileAccessor accessor4; 138:         FileAccessor accessor5; 139:         FileAccessor accessor6; 140:  141:         accessor1 = new FileAccessor (); 142:         accessor1.PrintFileContent (); 143:         accessor1.Dispose(); 144:         accessor1 = null; 145: 146:         accessor2 = new FileAccessor (); 147:         accessor2.PrintFileContent (); 148: 149:         accessor3 = new FileAccessor (); 150:         accessor3.PrintFileContent(); 151: 152:         accessor2.Dispose(); 153:         accessor2 = null; 154:         accessor3 = null; 155: 156:         accessor4 = new FileAccessor (); 157:         accessor4.PrintFileContent (); 158:         accessor4 = null; 159: 160:         accessor5 = new FileAccessor (); 161: 162:         MyFile.Reset(); 163:         accessor5 = null; 164: 165:         accessor6 = new FileAccessor (); 166:         accessor6.PrintFileContent (); 167:         accessor6.Dispose(); 168:         accessor6.Dispose(); 169:         accessor6 = null; 170:     } 171: } Access established to accessor number: 1 Content printed by accessor 1: Once upon a time a beautiful princess... File closed successfully by accessor number: 1 Access established to accessor number: 2 Content printed by accessor 2: Once upon a time a beautiful princess... Error! Accessor 3 could not access MyFile Accessor 3 has no access to MyFile File closed successfully by accessor number: 2 Access established to accessor number: 4 Content printed by accessor 4: Once upon a time a beautiful princess... Error! Accessor 5 could not access MyFile Access established to accessor number: 6 Content printed by accessor 6: Once upon a time a beautiful princess... File closed successfully by accessor number: 6 Error! Attempt to dispose accessor 6 more than once 

The virtual key mentioned in the previous overview is represented in the program by the number 321 and held by the const accessKey declared in line 6. When mobileKey is equal to 321, MyFile has the key; when mobileKey is zero, it doesn't have it. The Open method (lines 9 23) will give the key away (line 16) if MyFile holds the key (checked in line 11), after which it is set to not have it (line 13). If MyFile does not hold the key, the opening attempt fails (lines 20 and 21).

The key is given back to MyFile by calling the Close method (lines 25 37).

To read the contents of MyFile, the Read method (lines 39 50) must be called and the caller must provide the correct key (see formal parameter in line 39); the string of line 5 will then be returned.

Before attempting to Open MyFile, it can be handy to know whether it is occupied. The IsOccupied method (lines 52 58) provides us with that answer. The last method of MyFile, called Reset, can be called in case of gridlocks, for example, if the key has been lost by an object that became unreachable before it got the chance to give back the key.

The main task of the FileAccessor constructor (lines 74 89) is to establish access to MyFile. This is possible if IsOccupied is false (checked in line 78). The FileAccessor object then temporarily gets to hold the key in the accessKey variable (line 80). The bool variable disposed (line 69) is used to prevent the Dispose method from cleaning up more than once for the same FileAccessor object. This is achieved by preventing Dispose (lines 106 110) from invoking the cleaning up part of FreeState (lines 95 97) if dispose is true (line 93) combined with letting dispose be false after the constructor's successful connection (line 82) and by letting dispose be true when cleanup has taken place once (line 97).

Both the Dispose method and the destructor contain calls to FreeState in accordance with the Dispose design pattern, so to prevent FreeState from being called twice (once by Dispose and once by the destructor when called by the GC), the Dispose method calls the GC SuppressFinalize method (line 109) and asks for this object to be taken off the list of object destructors called by the GC.

The Main method of the TestFileAccess class creates six FileAccessor objects. Only one of those (accessor1 in lines 141 144) is correctly disposed and released. The Dispose method must always be called on an object (line 143) immediately after we have finished using it and must be made unreachable (line 144) immediately after the Dispose method has been called. Reversing this sequence will not work. The correctness of lines 141 144 is supported by the sample output, reporting that accessor2 has trouble-free access to MyFile in line 146. accessor3 is less fortunate during its access attempt in line 149 because of the missing call to the Dispose method of accessor2. This problem is rectified in lines 152 and 153.

A FileAccessor object can only connect to MyFile in its constructor, rendering accessor3 useless. Thus, line 154 releases the reference to accessor3.

The attempt to rely solely on the GC to clear accessor4's access to MyFile by simply making its referenced object unreachable in line 158 is likely to fail (as in this sample output) due to the infamous time lag previously discussed. Thus, accessor5's attempt to connect in line 160 fails. accessor4 brought the key with it into the unknown. Therefore, we must call the MyFile.Reset method to give back the key to MyFile.

Lines 165 168 demonstrate how the disposed variable of the FileAccessor class effectively prevents an instance from being cleaned up twice. The second call to Dispose in line 168 triggers the error message shown in the last line of the sample output.


   


C# Primer Plus
C Primer Plus (5th Edition)
ISBN: 0672326965
EAN: 2147483647
Year: 2000
Pages: 286
Authors: Stephen Prata

Similar book on Amazon

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