Wednesday, May 09, 2007 3:29 PM
Ever have a read only, expensive resource that everyone seems to want? Sure you have. Here's an easy way to optimize that access as well as give the garbage collector a hand, too.
First, lets take a look at our class which we'll be using to control our resource. ImageWrapper is a singleton which controls access to a single Bitmap file, named Sunset.jpg. Many people want to use Sunset.jpg but no one needs to edit it, in other words, all access is read only. This is a key point to remember for this example, this only works for read only scenarios. Now, let's take a look at ImageWrapper...
public class ImageWrapper : IDisposable
{
private static ImageWrapper _imageWrapper = new ImageWrapper();
private Image _image;
private static int _referenceCount;
private int _sizeOfImage;
private ImageWrapper()
{
// Create our image
string filePath = @"C:\Sunset.jpg";
_image = new Bitmap(filePath);
// The image is unmanaged, better let the garbage collector know
// that it's there
_sizeOfImage = File.ReadAllBytes(filePath).Length;
GC.AddMemoryPressure(_sizeOfImage);
}
~ImageWrapper()
{
Dispose(false);
}
public static ImageWrapper Instance
{
get
{
// One more reference to the image...
_referenceCount++;
return _imageWrapper;
}
}
public void Dispose()
{
// One less reference to the image...
_referenceCount--;
Dispose(true);
}
protected virtual void Dispose(bool calledFromDisposeMethod)
{
if (calledFromDisposeMethod)
{
// Is anyone else holding onto the image? If not, let's clean
// it up
if (_referenceCount == 0)
{
DestroyImageResource();
GC.SuppressFinalize(this);
}
}
else
{
// We were called from the Finalizer, this means that no one
// else is holding onto the Image so its safe to clean it up
DestroyImageResource();
}
}
private void DestroyImageResource()
{
// Clean up our image
_image.Dispose();
// Let the garbage collector know that its gone
GC.RemoveMemoryPressure(_sizeOfImage);
}
}
Wow, that's a lot of code. In fact, it looks like there's a lot going on here but as we break it down we'll see that it's actually not that bad. First, lets look at the structure. This class implements the Singleton pattern. Now a full discussion of the Singleton pattern is beyond the scope of this article, but in short, the Singleton assures us that there is ever only one instance of a given object at a time. The Singleton pattern is very cool in the sense that it's a very effective pattern with a clear purpose that's very easy to grok. In fact, this is often why it's most people's gateway pattern (not to be confused with
the Gateway Pattern). It's also for this reason that the Singleton is probably the most overused pattern ever. Everyone uses the Singleton pattern for everything and always qualifies it with 'but
this is a really good use of the Singleton pattern'. Now, you can find more about the Singleton pattern
here but for now just trust me...
this is a really good use of the Singleton pattern
Once we understand the Singleton pattern, let's take a look at how we're actually creating our one and only instance of the object in our private constructor.
private ImageWrapper()
{
// Create our image
string filePath = @"C:\Sunset.jpg";
_image = new Bitmap(filePath);
// The image is unmanaged, better let the garbage collector know
// that it's there
_sizeOfImage = File.ReadAllBytes(filePath).Length;
GC.AddMemoryPressure(_sizeOfImage);
}
Note that we're finding out the size of the file. Once we know it, we use the
GC.AddMemoryPressure(long) call to inform the garbage collector that additional data has been placed in memory. You see, bitmaps are unmanaged resources (Image is more orless a wrapper for the underlying GDI+ image manipulation classes) and the garbage collector only knows about managed resources. Therefore, we could easily create an Image class around a 500 megabyte file and the garbage collector would never be the wiser...it would only ever know about the reference to the Image class on the stack which could seriously skew its perceptions of the amount of memory available. AddMemoryPressure(long) solves this problem by giving a hint to the collector which allows it to schedule its collections a little more efficiently.
Next let's take a look at our Instance property and our Dispose() method.
public static ImageWrapper Instance
{
get
{
// One more reference to the image...
_referenceCount++;
return _imageWrapper;
}
}
public void Dispose()
{
// One less reference to the image...
_referenceCount--;
Dispose(true);
}
You've probably noticed that both of these members contain a reference to a member variable called _referenceCount. _referenceCount is a static integer that we increment every time someone accesses the ImageWrapper through the Instance property. Then each time they Dispose() of it, we decrement it. Why? This helps us keep track of how many people are currently using the underlying image, once our reference count goes to 0 we can safely remove the image because we can be sure that no one else is using it. If you've ever worked with classic COM and dealt with its reference counting system then this should feel familiar to you.
What happens if someone forgets to call Dispose()? Excellent question! Keen observers will have noticed that this class implements
IDisposable as well as implements the Dispose pattern. Like the Singleton, the Dispose pattern is a bit out of the scope for this article but curious minds can read up on it
here. The gist of the IDisposable interface is that it gives you an efficient way to quickly dispose of resources soon after consumers have finished with them. The gist of the Dispose pattern is that it gives you a way to ensure that your resource will still be disposed of even if your users forget to call Dispose()*.
That brings us to the final key of the ImageWrapper class.
protected virtual void Dispose(bool calledFromDisposeMethod)
{
if (calledFromDisposeMethod)
{
// Is anyone else holding onto the image? If not, let's clean
// it up
if (_referenceCount == 0)
{
DestroyImageResource();
GC.SuppressFinalize(this);
}
}
else
{
// We were called from the Finalizer, this means that no one
// else is holding onto the Image so its safe to clean it up
DestroyImageResource();
}
}
private void DestroyImageResource()
{
// Clean up our image
_image.Dispose();
// Let the garbage collector know that its gone
GC.RemoveMemoryPressure(_sizeOfImage);
}
This overload of Dispose() is what actually takes care of cleaning up our resource. Note that we check _referenceCount each time this method is called, once it gets to zero we know that no one else is using our resource and we can safely dispose of it. This keeps us from accidentally destroying the image while someone is still using it. If someone forgets to call Dispose() then no problem, we'll still clean up when this Dispose() is called from the
Finalizer (represented in C# by the thing that looks suspiciously like a destructor). Note that in DestroyImageResource() after we dispose of the image we call
GC.RemoveMemoryPressure(long). This informs the garbage collector that pressure we placed on the memory in the beginning has now been lifted so that it may relax its collections accordingly.
Here's how we would use the ImageWrapper class in practice...
class Program
{
static void Main(string[] args)
{
// Create some instances of the ImageWrapper
ImageWrapper imageWrapper01 = ImageWrapper.Instance;
ImageWrapper imageWrapper02 = ImageWrapper.Instance;
ImageWrapper imageWrapper03 = ImageWrapper.Instance;
// Use the ImageWrapper
// Clean up the ImageWrapper
imageWrapper01.Dispose();
imageWrapper02.Dispose();
imageWrapper03.Dispose();
}
}
All of these tiny little tricks are handy enough by themselves, but when used together they can really maximize the use of your resources when it matters the most.
*Well, almost ensure...see CriticalFinalizers for more details.