|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
In this article
IntroductionThis 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.)
.NET pooling with ObjectPoolWell, 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):
ObjectPool library in a nutshellThe 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 I am an IPoolableObject...Just before I begin, all interfaces, classes, etc. are defined in the
It is very straightforward to implement the // 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 // 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 PoolableAttributeBut I said almost. You have to do just one more thing. Apply to
the poolable class the [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 [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 ObjectPoolNow, 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 // 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 // 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 // poolable object instantiation version 2 (correct)
SampleObject __gc* obj = static_cast<SampleObject __gc*> (pool->Draw ());
Now, it's time to see the // 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 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 Creating the poolObject pools are created per type, e.g. class. Once you create an object
pool of type // 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 poolBefore 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 SampleObject __gc* obj =
static_cast<SampleObject __gc*> (pool->Draw ());
Returning objects to the poolPlease, 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 poolThere'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 Compiling ObjectPoolThere is TODO(s)
ConclusionThanks 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 Reporting bugsI 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! DisclaimerThe software comes “AS IS”, with all faults and with no warranties. If you find the source code useful thank me in your mind :)
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||