ObjectPool - a library for pooling .NET objects






4.79/5 (17 votes)
Oct 28, 2002
16 min read

138261

2394
A (managed C++) library for object pooling in .NET
- Download library source and examples - 19 KB
- Download compiled library and examples - 68 KB
- Latest source code and updated documentation (would be) available here soon.
In this article
- Introduction
- Object what?
- ObjectPool library in a nutshell
- Using the library (properly?)
- Compiling ObjectPool
- TODO(s)
- Conclusion
- Reporting bugs
- Disclaimer
Introduction
This article demonstrates how useful could be the .NET custom attribute classes and shows how to build your own pool of reusable objects, which resembles the one in COM+. You're expected to at least have heard about (.NET) attributed programming/custom attributes, and a bit of Managed C++ and a very little bit about reflection :) But if you don't -- don't worry! I don't understand Java! :)
Object what?
Object pooling. If you have ever written a poolable MTS/COM+ component, you can skip this funny intro and go directly to read ObjectPool library in a nutshell. Otherwise keep reading... What is a pool? A container, full with water, where fish swim. In our case, an object pool is a container, where objects "swim" :) No, seriously, the object pool is a container, which not only allows objects to be drawn from, and returned back, but also creates them on the fly, whenever you want to draw more objects than you have at hand. When you need a new object, the pool searches for a free object to give you. If the "fish" is not found in the pool, the pool "gives birth" to a bunch of brand new objects and hands you one of them. If a free one was found, the pool just gives it to you. "But why", you'll wonder, "do I need an object pool that creates objects? Can't I just instantiate as many objects as I wish?". My answer is: Yes, you could. But not in all cases. There are some special cases, where to just pull the caught fish from the bucket is better (and definitely faster) then to catch a new fish.
Reusing objects (not classes)
If all objects were small and fast, the programmer would die from happiness, that's why the world serves us heavy tasks which need heavy objects. And heavy not only means that an object has many data embedded in properties and data structures, but also means heavy initialization code. Imagine you have a bunch of Dictionary objects, which are essentially the same, but are used to translate different languages. Well, they could easily be written as a single class, which takes an argument -- the desired language. The object then, connects to a database, pulls N megabytes, stores it in some internal data structures and is ready to be used. Now imagine that you should create a new object for every instance of its client. Well, I guess you don't want to waste a minute or so for each Dictionary creation, do you? So the problem is apparent, but the solution not yet. If there were some mechanism which allowed you to create the objects, store them anywhere, put them to sleep, and wake them only when you need them, you wouldn't have any problem, right? (Maybe.:) Right! Here's were object (not class) reuse come to help you. I've seen several implementations of object pooling, of which the best one is Microsoft's implementation of COM+ components pooling. I'll not compete with Microsoft (yet:), but will give the first (known to me) implementation of object pooling for .NET objects. So read along and enjoy...
History (or about COM+ pooling)
(This subsection's text is copy/pasted from Platform SDK/Component Services/
Services Provided by COM+/Object Pooling and is copyrighted (c) material of Microsoft corp.)
Object pooling is an automatic service provided by COM+ that enables you to configure
a component to have instances of itself kept active in a pool, ready to be used by any
client that requests the component. You can administratively configure and monitor the
pool maintained for a given component, specifying characteristics such as pool size and
creation request time-out values. When the application is running, COM+ manages the pool
for you, handling the details of object activation and reuse according to the criteria
you have specified.
You can achieve very significant performance and scaling benefits by reusing objects in
this manner, particularly when they are written to take full advantage of reuse. With
object pooling, you gain the following benefits:
- You can speed object use time for each client, factoring out time-consuming initialization and resource acquisition from the actual work that the object performs for clients.
- You can share the cost of acquiring expensive resources across all clients.
- You can pre-allocate objects when the application starts, before any client requests come in.
- You can govern resource use with administrative pool management—for example, by setting an appropriate maximum pool level, you can keep open only as many database connections as you have a license for.
- You can administratively configure pooling to take best advantage of available hardware resources—you can easily adjust the pool configuration as available hardware resources change.
- You can speed reactivation time for objects that use Just-in-Time (JIT) activation, while deliberately controlling how resources are dedicated to clients.
.NET pooling with ObjectPool
Well, as I mentioned in the Reusing objects section
I'm too little to compete with Microsoft, but of the features listed above, the
ObjectPool library can (cut-down version of the Marketing blah-blah):
- speed object use time...
- share the cost of acquiring expensive resources...
- pre-allocate objects when the application starts...
- construct (configure) objects, like COM+ (not mentioned above)
ObjectPool library in a nutshell
The library is as little as cool, or as cool as little, whichever sounds better, and in my favor:) It consists of one interface, one attribute class, and two classes, of which one is just a helper. But before I dive deeply into the library's implementation, I'll explain that there is no free lunch, and free poolable objects.
Poolable objects must meet some requirements to enable a single object instance
to serve multiple clients. For example, they can't hold client state (internal
object data, persisted only while serving a particular client) or have any thread
affinity (as the object will be reused from different threads). In addition, all
objects that wish to become poolable must implement the IPoolableObject
interface, and be marked with the [PoolableAttribute]
attribute. Inheriting the
IPoolableObject
interface enables the object to perform initialization
when activated and clean up any client state on deactivation. Furthermore, object
are notified when they are created for the first (and last) time, as well as when
they are destroyed by the library and left alone with the Evil GC :). Often, it useful
to write poolable objects in a somewhat generic fashion (like the Dictionary objects I
mentioned above) so that they can be customized with some configuration state which will
make them look distinct. For example, an object might be written to hold a generic SQL
Server client connection, with a preliminary configured construction string. Well,
unlike COM+, we have more freedom here, as this is a library and not a runtime
framework, so our poolable objects could be initialized with any Object __gc*
-
derived (or boxed __value
) class instance.
I am an IPoolableObject...
Just before I begin, all interfaces, classes, etc. are defined in the
ObjectPooling
namespace, so I'll omit it in the article.
It is very straightforward to implement the IPoolableObject
(which
so much resembles IObjectControl
that I'm ashamed). Here's its
definition:
// the interface which each poolable object should implement
// to qualify as poolable (similar to COM+ IObjectControl)
public __gc __interface IPoolableObject
{
public public:
// called by the ObjectPool on each object, which needs
// to be configured
void Construct (Object __gc* configuration);
// called at first object creation
void Create ();
// called at each activation (drawing from the pool)
void Activate ();
// called at each deactivation (return in the pool)
void Deactivate ();
// called at object destruction (leaving it to GC)
void Destroy ();
}; // IPoolableObject
As I said, it is very straightforward, but its tedious too. So I've written
a very simple implementation, and named it BasePoolableObject
.
The class just implements overridable stubs, for its inheritors. The only useful
thing it does alone is persisting the configuration object and exposing it as
its property. Here's its implementation:
// helper class, implementing the IPoolableObject interface
// most poolable objects will derive from it and will override
// its methods
public __gc class BasePoolableObject : public IPoolableObject
{
protected protected:
BasePoolableObject () :
m_configuration (0) // unconfigured by default
{
}
public public:
// called by the ObjectPool class at runtime
// not overridable
void Construct (Object __gc* configuration)
{
this->m_configuration = configuration;
}
// overridable methods
virtual void Create ()
{
}
virtual void Activate ()
{
}
virtual void Deactivate ()
{
}
virtual void Destroy ()
{
}
// properties
__property Object __gc* get_Configuration ()
{
return (m_configuration);
}
public protected:
Object __gc* m_configuration;
}; // BasePoolableObject
An (almost) poolable object could now be created with no bigger effort than typing the following line of code:
public __gc class SampleObject : public BasePoolableObject {}
... so mark me with the PoolableAttribute
But I said almost. You have to do just one more thing. Apply to
the poolable class the PoolableAttribute
attribute, or in C#
-- add [Poolable]
on top of your class' declaration. However,
you can control some interesting aspects of the pooling, so I'll give you
some source code of the PoolableAttribute
class. But even
before I show you the code, you'll need to know, that the attribute class
itself is marked with a .NET custom attribute -- the AttributeUsageAttribute
one. It is needed to mark our Poolable
as applicable only to
classes, and mark it for single use.
[AttributeUsageAttribute (
AttributeTargets::Class, // supported on classes only
AllowMultiple = false)] // and only once
Now, as I'm writing this article, I'm dreaming of the time I'll have some
more free time to write for fun, I thought of having multiple ways to pool
objects in the object pool, so don't worry when you see the PoolingType
property of the attribute. It's here -- but its not take into consideration (yet).
[FlagsAttribute]
public __sealed __value enum PoolingType : int
{
// normal pooling
NormalPooling = 0x0000001,
// pool with weak references
WeakReferences = 0x0000002,
// both
WeakPooling = NormalPooling | WeakReferences,
// the pool decides at runtime how to pool the objects
Runtime = 0x0000004
};
// the cool attribute that provides information to the
// ObjectPool class
[AttributeUsageAttribute (
AttributeTargets::Class, // supported on classes only
AllowMultiple = false)] // and only once
public __gc class PoolableAttribute : public Attribute
{
public public:
// constructor (and overloads below)
PoolableAttribute (
PoolingType poolingType, // not used (yet)
bool constructable, // supports construction/configuration
int minPoolSize, // minimum pool size
int preferedPoolSize, // preferred pool size (not used yet?)
int maxPoolSize, // pool threshold
int creationTimeout) : // object creation timeout
m_poolingType (poolingType),
m_constructable (constructable),
m_minPoolSize (minPoolSize),
m_preferedPoolSize (preferedPoolSize),
m_maxPoolSize (maxPoolSize),
m_creationTimeout (creationTimeout)
{
CheckState ();
}
// overloaded constructors omitted for brevity...
// properties
__property PoolingType get_Pooling ()
{
return (m_poolingType);
}
__property void set_Pooling (PoolingType poolingType)
{
m_poolingType = poolingType;
CheckState ();
}
private private:
void CheckState ()
{
if (m_minPoolSize > m_preferedPoolSize ||
m_minPoolSize > m_maxPoolSize ||
m_preferedPoolSize > m_maxPoolSize)
throw (new ArgumentException (
S"The condition min <= prefered <= " +
S"max pool size was not met");
if (m_creationTimeout < -1)
m_creationTimeout = InfiniteCreationTimeout;
}
PoolingType m_poolingType;
bool m_constructable;
int m_minPoolSize;
int m_preferedPoolSize;
int m_maxPoolSize;
int m_creationTimeout;
// default values -- change at will :)
static const bool DefaultConstructable = false;
static const PoolingType DefaultPoolingType =
PoolingType::NormalPooling;
static const int DefaultMinPoolSize = 1;
static const int DefaultPreferedPoolSize = 16;
static const int DefaultMaxPoolSize = 100000;
static const long int DefaultCreationTimeout = 60000;
public public:
static const long int MaxCreationTimeout =
0x80000000; // ~2 billions
static const long int InfiniteCreationTimeout = -1;
}; // PoolableAttribute
... and instantiate me via the ObjectPool
Now, when we're done talking, I'll show you a real poolable class in a couple of lines. It's not smart, it's not doing anything cool, it's just a poolable class I actually use in the sample (in the source code). Here it is:
[PoolableAttribute (
PoolingType::NormalPooling, // not used (yet)
true, // constructable
5, // 5 objects will be initially created in the pool
16, // prefered pool size is 16 objects (so what? :)
18)] // the pool will exhaust when 19th object is
// requested and none were returned in the pool
public __gc class SampleObject : public BasePoolableObject
{
public public:
// override
void Create ()
{
// do some very serious (heavy) stuff right here
}
// just a simple method showing that the object is working
void DoWork ()
{
Console::WriteLine (S"...");
}
}; // SampleObject
Yours could be a two-liner, e.g.
[PoolableAttribute(PoolingType::NormalPooling, true, 5, 16, 18)]
public __gc class SampleObject : public BasePoolableObject {}
Now you've seen how to create a poolable class, but you haven't
seen how to use it, right? Now is the time! All object creation requests
are handled through the ObjectPool
class. It maintains
instances of the poolable class in a pool, ready to be activated for
any client requesting the object, just like the COM+ object manager.
And because, I'm better in writing code, than "talking" look
how easy it is to create an object pool:
// ain't it cool? :)
ObjectPool __gc* pool = new ObjectPool(__typeof(SampleObject));
But you haven't created the pool already:) I intentionally provided a method for creating and destroying an object pool to avoid the overhead if you're planning to use it but it is possible that you won't. So the pool is not actually created in the constructor, but could be very easily created like in the code below:
// 5 objects will be created and will wait
// to be drawn from the pool
pool->CreatePool (S"Hello World!"); // configure newly created
// objects with this string
// BTW, I made this method to return a pointer to the object
// pool, so the lazy guys (like me) could write:
ObjectPool __gc* pool = (new ObjectPool ())->CreatePool(S"...");
By the way, the pool can work in two modes: "exhaustible" and "non-exhaustible". In the former mode, when there are no more free objects in the pool, but a request to draw one is executed, the pool throws an exception. In the non-exhaustible mode, the pool blocks, waiting for a free object to be returned, and returns it to the client. If one properly uses the pool (i.e. draws objects only when she needs them, and returns them when she's finished using them), she wouldn't need the pool to work in non-exhaustible mode (?). However, under a very heavy load, the pool may really get exhausted, and throwing exceptions is not a very cool solution, so I made the pool working in one of the aforementioned modes. Here are some examples of creating both kinds of pools:
// method definition (3 overloads skipped, see their usage below)
ObjectPool __gc* CreatePool (Object __gc* configuration,
bool canExhaust);
// exhaustable, with no configuration
pool->CreatePool ();
// non-exhaustable, no configuration
pool->CreatePool (false);
// exhaustable, a stack for configuration
pool->CreatePool (new Stack ());
// non-exhaustable, string configuration
pool->CreatePool (false, S"Hello");
Now, when we have the object pool up and ready, we're ready in our turn
to go "fish". The only thing, that is easier than the pooled
object instantiation is the normal instantiation via the new
operator:) But here's how you'd instantiate an object via the pool:
// poolable object instantiation version 1
SampleObject __gc* obj = pool->Draw ();
I'm lying. It's not that easy, as the objects are created at runtime via a mechanism, known as reflection. Look at the method below to see why:
// helper function, which creates poolable objects
IPoolableObject __gc* CreatePoolableObject ()
{
IPoolableObject __gc* po =
static_cast<IPoolableObject __gc*> (
Activator::CreateInstance (m_type));
// construct the object if needed
if (m_constructable)
po->Construct (m_configuration);
// call object's create method
po->Create ();
return (po);
}
Because the CreateInstance
method of the Activator
class returns Object
, the only thing I could do better is to
cast it to IPoolableObject
, as I know that only these kinds of
objects are present in the pool. That's all I can do. Well, to convert it to
SampleObject
, we need at least (and enough) a static_cast
,
like in the example below:
// poolable object instantiation version 2 (correct)
SampleObject __gc* obj = static_cast<SampleObject __gc*> (pool->Draw ());
Now, it's time to see the ObjectPool
class, isn't it? OK, but before your rush,
I'm warning you that I deleted the "uninteresting" code from the listing
below, so take a look at the "ObjectPool.h" file for details:
// constructor
ObjectPool (Type __gc* type)
: m_type (type), // type from which to create objects
m_canExhaust (true),
m_created (false), // no created yet
m_pool (0), // ...
m_freeObjects (0), // ...
m_canPoolType (false) // unknown at this time
{
// check if the type implements the IPoolableObject interface
if (m_type->IsSubclassOf (__typeof (IPoolableObject)))
throw (new ArgumentException (
S"The type should derive from IPoolableObject",
S"type"));
m_sync = new SyncGuard (this);
// obtain the PoolableAttribute and its properties
ObtainTypeInformation ();
}
// ...destructor skipped...
// creates the actual pool
ObjectPool __gc* CreatePool (
Object __gc* configuration,
bool canExhaust) // see m_canExhaust for explanation
{
Trace (S"CreatePool: Creating pool");
if (m_created)
throw (new InvalidOperationException(
S"Pool has been already created"));
m_sync->Lock ();
m_canExhaust = canExhaust;
m_configuration = configuration; // save configuration
// create helper objects with capacity = the prefered size
m_pool = new Hashtable (m_preferedSize);
m_freeObjects = new Stack (m_preferedSize);
m_created = true;
m_sync->Unlock ();
// create only "minimum pool size" objects
Grow (m_minSize); // create initial pool with minimum size
return (this);
}
// ...overloads skipped...
// destroys the pool
void DestroyPool ()
{
Trace (S"DestroyPool: Destroying pool");
if (! m_created)
throw (new InvalidOperationException (
S"Pool cannot be destroyed, becuase " +
S"it has not been created"));
m_sync->Lock ();
IDictionaryEnumerator __gc* dict = m_pool->GetEnumerator ();
// walk all objects, deactivate and destroy them
while (dict->MoveNext ())
{
IPoolableObject __gc* po =
static_cast<IPoolableObject __gc*> (dict->Value);
po->Deactivate ();
po->Destroy ();
}
// clear internal helper objects
m_pool->Clear ();
m_freeObjects->Clear ();
m_created = false;
m_sync->Unlock ();
}
// Draws an object from the pool. If there aren't any objects
// created, a new one is created, otherwise an existing one
// is reused.
IPoolableObject __gc* Draw ()
{
Trace (S"Draw: Drawing object");
if (! m_created)
throw (new InvalidOperationException (
S"Pool has not been created"));
// check if type can be pooled
if (! m_canPoolType)
{
// can't activate the object, but at least can configure it
Trace (S"Draw: Type not poolable, creating it");
return (CreatePoolableObject ());
}
// this one is a poolable type
// search for available item in the pool and return it
IPoolableObject __gc* po = InternalDraw ();
if (po != 0)
{
Trace (S"Draw: Object activated and used from the pool");
return (po);
}
else // no free ones
{
Trace (S"Draw: No free objects, trying to grow");
if (Grow (-1))
{
// after the grow there will be free ones now
return (InternalDraw ());
}
else
{
if (m_canExhaust)
{
throw (new InvalidOperationException (
S"Pool exhausted!"));
}
else // the pool should wait until a free object returns
{
Trace (S"Draw: Waiting for free object " +
S"to return in the pool");
while (po == 0)
{
po = InternalDraw ();
Thread::Sleep (0); // give just a bit of time
}
Trace (S"Wait finished. At least " +
S"one free object returned.");
return (po);
} // if (m_canExhaust)
} // if (Grow (-1))
} // if (po != 0)
} // Draw
// Returns an object to the pool to be reused
void Return (IPoolableObject __gc* object)
{
Trace (S"Return: Returning object in the pool");
if (! m_created)
throw (new InvalidOperationException (
S"Pool has not been created"));
if (! m_pool->ContainsKey (object))
throw (new InvalidOperationException (
S"Object was not created by this object pool"));
if (! m_canPoolType)
{
Trace (S"Return: Object not poolable, " +
S"leaving GC do its work");
return; // can't do anything with
//unpoolable objects, but GC can
}
// cleanup and return the item in the pool
object->Deactivate ();
m_sync->Lock ();
m_freeObjects->Push (object);
Trace (S"Return: Object deactivated and " +
S"returned in the pool");
m_sync->Unlock ();
} // Return
// ... trace methods skipped ...
void ObtainTypeInformation ()
{
Trace (S"ObtainTypeInformation: Obtaining type information");
m_sync->Lock ();
// get the type's custom attributes
Object __gc* attArray __gc[] =
m_type->GetCustomAttributes (true);
Attribute __gc* atts __gc[] =
static_cast<Attribute __gc* __gc[]> (attArray);
// search for our PoolableAttribute
for (int i=0; i<atts->Length; i++)
{
if(atts [i]->GetType () == __typeof(PoolableAttribute))
{
// found?
m_canPoolType = true; // type can be pooled
// retrieve pooling information
PoolableAttribute __gc* pt =
static_cast<PoolableAttribute __gc*> (atts [i]);
m_constructable = pt->Constructable;
m_poolingType = pt->Pooling;
m_minSize = pt->MinPoolSize;
m_preferedSize = pt->PreferedPoolSize;
m_maxSize = pt->MaxPoolSize;
m_creationTimeout = pt->CreationTimeout;
break; // no need to search further
}
}
m_sync->Unlock ();
} // ObtainTypeInformation
// Searches for a free item in the pool. If one is found, it is
// returned, otherwise the method returns 0 (null) to notify the
// main Draw method (which would eventually grow the pool)
IPoolableObject __gc* InternalDraw ()
{
m_sync->Lock ();
// check for free objects
if (m_freeObjects->Count > 0) // found?
{
// get one
IPoolableObject __gc* po =
static_cast<IPoolableObject __gc*>(
m_freeObjects->Pop ());
m_sync->Unlock ();
// ... and activate it
po->Activate ();
Trace(S"InternalDraw: Free object found and activated");
return (po);
}
m_sync->Unlock ();
Trace (S"InternalDraw: Could not find a free object");
return (0); // no free object found
} // InternalDraw
// grows the pool as needed : creates objectCount
// number of objects
bool Grow (int objectCount)
{
Trace (S"Grow: Attempting to grow");
m_sync->Lock ();
// -1 means, that the pool should decide how many objects it
// can create
if (objectCount == -1) // calculate the number of objects
objectCount = Math::Min (m_maxSize - m_pool->Count,
DefaultGrowSize);
if (objectCount <= 0) // pool has exhausted
{
Trace (S"Grow: Pool exhausted. Can't grow pool");
m_sync->Unlock ();
return (false);
}
Trace (S"Grow: Creating and constructing {0} objects",
__box (objectCount));
for (int i=0; i<objectCount; i++)
{
// create brand new objects
IPoolableObject __gc* po = CreatePoolableObject ();
// activated at InternalDraw
m_pool->Add (po, po); // add to pool
m_freeObjects->Push (po); // and to the free obj list
}
m_sync->Unlock ();
Trace (S"Grow: Grown successfully");
return (true);
} // Grow
// ... CreatePoolableObject() skipped -- show somewhere above ...
// pool's internal properties
Type __gc* m_type; // type of objects to create
// If a pool can exhaust, then when its limit is reached
// (i.e. there are no free objects, and an attempt to get
// one is made), the pool throws a PoolExhausted exception.
// Otherwise, if a pool is not exhaustable, then when its
// limit is reached, the pool blocks until a free object is
// avaiable, and returns it to the caller
bool m_canExhaust;
// synchronization object
SyncGuard __gc* m_sync;
// indication of the pool status
bool m_created;
// helper to store the objects in
Hashtable __gc* m_pool;
// a free list of objects
Stack __gc* m_freeObjects;
// indication whether the type is poolable
bool m_canPoolType;
// saved configuration from the Create method
Object __gc* m_configuration;
// pool properties, based on the Pooling type
// these are UNITIALIZED in the constructor, because we
// don't know how to initialize them
PoolingType m_poolingType; // see the PoolableAttribute
bool m_constructable;
int m_minSize;
int m_maxSize;
int m_preferedSize;
int m_creationTimeout;
static int DefaultGrowSize = 16; // default pool grow size
I don't think that the synchronization will be interesting to anyone, so I'll
say only that it uses the Monitor
class from the
System::Threading
namespace. You can peek at the code and see how
simple it is. Initially I thought I should use the ReaderWriterLock
class, but after a little thought (and a bang on the head by a friend:) I dropped
it and stuck to the sufficient Monitor
class.
Using the library (properly?)
Well, I guess you've used it already, reading the topics above. If you haven't read them and jumped right here, I'd suggest that you go back and read them. However, I'd like to say a couple of words about the (proper) use of the library. So read ahead...
[Poolable] needs to be smart?
.NET attribute classes are not dynamic, so they can't be smart. I mean that
before you place that [Poolable]
attribute you should think how
many instances are likely to be used initially, on the average and their maximum.
Once you apply the attribute to the class, you'll have to recompile it when
you change it, ok? So you'll have to carefully choose the values for
minPoolSize
, preferedPoolSize
and maxPoolSize
,
as well the timeout for the (initial) object creation. If you don't want to set
a creation timeout, simply pass InfiniteCreationTimeout
. By default,
all objects are attempted to be created in 60 seconds. If the creation fails
because the object did not respond in a timely manner, the object is discarded
as if it were never created. But relax :), I'll implement the object creation
timeout next week, because I'm very busy right now. So you can expect that your
objects will be created no matter how much time they waste...
Creating the pool
Object pools are created per type, e.g. class. Once you create an object
pool of type SomeType
, you cannot draw object of
AnotherType
, or return AnotherType
object in the
SomeType
pool. Sounds restrictive, but that's the proper way.
The other way is a way to the programming hell, believe me. This is not
your old (?) COM+, and I'm not Microsoft (though I'd like to work there
one day :) As you can see from the ObjectPool
source above,
when the ObjectPool
class is instantiated, no pool is really
created, until you call the Create
method.
// does not actually create a pool of objects, just prepares
// the internal state of the object pool
ObjectPool __gc* pool =
new ObjectPool (__typeof (SampleObject));
// the line below creates the pool
Hashtable __gc* objectState = new Hashtable ();
objectState->Add (S"key1", s"Value1");
// ... some more...
// configure objects with this hash table
pool->CreatePool (objectState);
Drawing objects from the pool
Before you write the code to draw an item from the pool, write the
code for returning it to the pool. If you forget to return the object
to the pool, nothing bad will happen to Windows, the CLR and probably
your application. What WILL happen is that you'll loose the object.
The object pool keeps track of the objects, so at its destructor
it properly deactivates and destroys ALL objects, no matter if they
were returned to the pool or not. But the object will not be in the
"free list", so it will be unusable. Again, be sure to specify
a good initial number of objects that need to be created to avoid the
penalty of creating objects at critical times during the runtime of
your application. (Also, you can change the DefaultGrowSize
constant in the source code of the ObjectPool
class or
even expose it as the PoolableAttribute
's property. If you
forgot how an object is drawn from the pool, here's the code:
SampleObject __gc* obj =
static_cast<SampleObject __gc*> (pool->Draw ());
Returning objects to the pool
Please, return them :) Don't let them get lost, as later, the Evil GC will find them, and then who knows? :) I'll provide the "return object" construct only because I know most of you have skipped (some of) the previous sections.
pool->Return (obj); // nothing more simple
Destroying the pool
There's no problem if you forget to destroy the pool. When the GC gets it later,
its destructor will deactivate and destroy all pooled objects. However, if your
poolable class uses unmanaged resources, and releases them in its Destroy
or Deactivate
methods, it would be nice to destroy the pool. The code
to do that is just not worth to type.
Compiling ObjectPool
There is Src
folder, containing 4 other folders:
Bin
, ObjectPooling
, ObjectPoolingTest
and CSharpPoolingTest
. When you build the solution file, all
executables and assemblies will go into the Bin
folder. The
ObjectPooling
folder contains the library's source files, the
ObjectPoolingTest
folder contains an example of ObjectPool library
client, and the CSharpPoolingTest
-- guess what:)
Now, I won't excuse for not building a command-line batch file for building the
library and the sample application. It's fairly easy to write your own, as the
library only uses mscorlib.dll
and System.dll
. If you
have VS.NET or VC++ .NET, just build the solution. I'm a very busy boy, so I
never have the time to write such batch files, sorry!
TODO(s)
- There are two things I haven't done (or done partially):
- Object creation timeout is not inspected and creation is not monitored or done asynchronously, though a simple thread, pulsing an event would do a very good job, but... next week I guess.
- The pooling with the
WeakReference
is not implemented, as I'm just figuring out how to do it:) The idea, by the way, is Jeffery Richter's so I'm giving him his credit right here and right now -- Thanks Jeff!
Conclusion
Thanks for reading the article! I was expecting that some people would peek at it, and I'm glad the YOU have actually read the whole of it, thanks again!
Object pooling is a very old programming paradigm, but I remember since
MTS that very few programmers take advantage of it. Furthermore, pooling
COM+ components was not very recommended for VB COM components, so only the
C++ guys could make their objects poolable, fast, free-threaded, etc. (And
I was a VB programmer once :) Now, you can pool your .NET objects not only with the
runtime provided for COM+ components written in .NET languages, but for
every simple object that deserves such a special attention. Of course, I'm
sure there are many guys out there that could laugh at the implementation,
suggest super-duper data structures beside the hash table and the stack
I've used. I'll tell one thing -- CodeProject is to teach and learn, not to
show up how COOL you (may think you) are, and the .NET Hashtable
and Stack
classes are present in every .NET-enabled :) machine.
Reporting bugs
I don't know any (as I just wrote the article:), so if you find one, please, let me know. My e-mail address is at the bottom of the article (and an alternative one is stoyan_damov[at]hotmail.com, but PLEASE!!! do not spam me. Thanks!
Disclaimer
The software comes “AS IS”, with all faults and with no warranties.