Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Object Pooling using C#

0.00/5 (No votes)
29 Dec 2005 1  
An article on object pooling using .NET.

Preface

In software development, we often face problems while our application is under high stress. Most of the problem is due to issues in memory management. So developers should take some extra care so that an application will be optimized and follows proper memory management principles.

Introduction

Object Pooling is a service that is documented in MTS but is fully supported by Microsoft .NET. The MTS Glossary defines Pooling as "A performance optimization based on using collections of pre-allocated resources, such as objects or database connections." It is also mentioned that pooling results in more efficient resource allocation.

Background

Let�s assume an application which needs to allocate some memory for its full execution and this application is multithreaded. This means simultaneously more threads can allocate the same amount of memory. Consider that the application needs 100K memory for its execution and there are 50 threads running simultaneously. All 50 threads will call a method of the application simultaneously.

All the threads will try to allocate 100000 bytes in the heap simultaneously. The OS may not get enough time to swap pages. Thus the application will be under heavy concurrent access, and the application may fail due to misleading memory management!

An interesting question that can come to your mind regarding the use of pinning (i.e. GC.AddrOfPinnedObject()).

Solution to the problem

There are two solutions for this problem:

  1. We can request 50 times 100K bytes at runtime suddenly. But here the usage of memory will be huge.
  2. WE can also create a pool of as many as the expected high volume byte arrays in the heap, at startup. That way, say we create 100 times 100K bytes in memory at startup. If it fails, well enough - we know that right at startup! So there is no surprise at runtime, when the application is live! So we can reduce that number to 75 (from 100) and try again to start our application. Once up and running, we know that there will be barely a need for it to allocate more memory at runtime, as we already have the memory for 75 or 100 arrays allocated. It just uses that as "an object pool", pulls from it, and when done, returns the byte arrays for subsequent use. Chances are that unless truly "concurrent", the already allocated byte arrays will be re-used over and over again, and at runtime, no extra memory will be allocated. If all 75 or 100 arrays are in use, and a 101st request comes in, it will need to allocate an additional memory of only one array - which is easy for the OS to manipulate in RAM.

So the second method is preferable because this process will use the memory in a more optimized way, by which we can achieve our target of concurrent access.

Using the code

Implementing object pooling involves creating a "queue" which is a wrapper class that will hold, at runtime, the initially allocated 75 or 100 byte arrays. Each thread, when needed, will request a byte array from it. It will traverse its list of arrays to find out which one is "free". It will give that array for usage to the thread, and mark it as "locked" (so that it does not give the same array to the next requestor). When this thread is done, it will notify the queue, and the queue will simply make the array available for the next requestor. If a request for an array comes at a point of time when there are none free, then the queue object will create an additional array and give it back to the byte pool, increasing the size of the pool by 1.

// Byte Array class

public class ByteArray
{
     //Variable declaration 

     private byte [] byArray;
     private int iAddress;
     private bool bLocked;
     private GCHandle GC;

     //Constructor of the class which will 

     //instantiate byte array with given size.

     public ByteArray( int iMaxSize )
     {
             byArray = null;
             iAddress = 0;
             bLocked = false;
             try 
             {
                byArray = new byte [ iMaxSize ];
             }
             catch( Exception ex )
             {
                throw new Exception( "Error trying" + 
                      " to create byte array: " + ex.Message );
             }
     }
 
     //Get byte array

     public byte [] GetByteArray() 
     { 
         return byArray; 
     }
      
     //Get the address of Byte Array

     public int GetAddressOfByteArray()
     { 
          return iAddress; 
     }

     //Initialize the byte array 

     private void Reset() 
     { 
          byArray.Initialize(); 

     }

     //Lock the byte array which is allocated to any thread 

     public bool Lock()
     {
             ...
     }
 
     //Unlock the byte array which is locked, 

     //so that it can be use by any other threads. 

     public void Unlock()
     {
             ...
     }

     //Check whether the byte array is locked or not ! 

     public bool IsLocked() 
     { 
         return bLocked; 
     }
}

The initial size (MaxConcurrentHits) of the pool is very important and this number should only be the maximum number of concurrent hits. Under a steady (along the time axis) load, even if it is high volume, concurrency matters - so keeping it to a value like 100 is absolutely safe.

At the same time, the maximum memory size (MaxSize) is also very important. Because each byte array is allocated a size equal to that. For different purposes, set a different value.

// PoolByteArrays class definition

public class PoolByteArrays
{
     private ArrayList byarArray;
     private int iCurrentPoolSize;
     private int iMaxByteArraySize;

     //Constructor of PoolByteArrays class 

     //which takes input as MaxConcurrentHits and MaxSize

     public PoolByteArrays( int iMaxConcurrentThreads, int iMaxSize )
     {
         byarArray = null;
         iCurrentPoolSize = 0;
          iMaxByteArraySize = iMaxSize;

         if( iMaxConcurrentThreads < 1 )
            throw new Exception( "Construction of " + 
                  "PoolByteArrays failed - iMaxConcurrentThreads" + 
                  " is less than 1" );

         try
         {
              byarArray = new ArrayList( iMaxConcurrentThreads );
              for( int iCnt = 0; iCnt < iMaxConcurrentThreads; iCnt++ )
                byarArray.Add( new ByteArray( iMaxSize ) );

              iCurrentPoolSize = iMaxConcurrentThreads;
         }
         catch( Exception ex )
         {
              throw new Exception( "Construction of" + 
                    " PoolByteArrays failed - " + ex.Message );
         }
     }

     //Get base address of free Byte array

     public int GetAddressOfAFreeByteArray( ref int iAddress )
     {
         try
         {
              for( int jCnt = 1; jCnt <= 2; jCnt++ )
              {
                   for( int iCnt = 0; iCnt < iCurrentPoolSize; iCnt++ )
                   {
                        ByteArray byarInstance = ( ByteArray ) byarArray[ iCnt ];

                        if( byarInstance.IsLocked() )
                                continue;
                        if( ! byarInstance.Lock() )
                                continue;

                        iAddress = byarInstance.GetAddressOfByteArray();
                        return iCnt;
                   }
              }
         }
         catch( Exception ex )
         {
             throw new Exception( "Exception trying" + 
                   " to locate and lock a free byte array." + 
                   " Details: " + ex.Message );

         }
         ...
         ...
     }
 
     //Convert ByteArray to String

     public string GetStringStoredInByteArray( int iID )
     {
        return( ( new ASCIIEncoding()).GetString( ( ( ByteArray ) 
          byarArray[ iID ] ).GetByteArray() ) ).TrimEnd( new char [] { 
                       Convert.ToChar( ( int ) 0 ) } );
     }

     //Unlock the locked byte array and make it available for others

     public void DoneWithByteArray( int iID )
     {
         try
         {
            ( ( ByteArray ) byarArray[ iID ] ).Unlock();
         }
         catch( Exception ex )
         {
            throw new Exception( "Exception trying to unlock" + 
                  " a byte array. Details: " + ex.Message );
         }
     }

     //Destroy the byte pool

     public void DestroyPool()
     {
         for( int iCnt = 0; iCnt < iCurrentPoolSize; iCnt++ )
         {
              ByteArray byarInstance = ( ByteArray ) byarArray[ iCnt ];
              byarInstance.Unlock();
         }
         byarArray.Clear();
     }
}

An application which implements the above classes can have a source code as below:

//Declare PoolByte array

public PoolByteArrays poolByteArrays;
...
//initialize the above array with null

poolByteArrays = null;
...
...
//Create Object Pool

//Let's assume MaxConcurrentHits = 100

//MaxMemorySize = 2000 bytes

poolByteArrays = new PoolByteArrays( 100, 2000 );
...
int iAddressOfAByteArray = 0;
//Get the base address of free byte array

int iByteArrayID = 
    poolByteArrays.GetAddressOfAFreeByteArray( ref iAddressOfAByteArray );
...
//Call method of Unmanaged DLL method

string strOutput = "";
unsafe
{
     //Call method of Unmanaged DLL method with some return character pointer

     CallUnmanagedFunction(iAddressOfAByteArray);
     //Convert Byte array to string data type

     strOutput = poolByteArrays.GetStringStoredInByteArray( iByteArrayID );
}
...
//Free the used Byte array for others

poolByteArrays.DoneWithByteArray( iByteArrayID );
...
//Destroy the object pool

poolByteArrays.DestroyPool();

Points of Interest

In the Microsoft .NET framework, the Garbage Collector (GC) plays an important role in releasing non-reference allocated memory in regular intervals. After the GC frees the required memory, it does a defragmentation and changes the base address of the referred memory location. But there could be some scenario where a base address of a memory location is referred to by a certain object or variable and after defragmentation, the same base address of the assigned memory is required. How can developers keep this address unchanged?

To pin the base address, the developer has to pin the memory at the time of allocation by which GC can know that this memory location is already pinned (using GCHandleType.Pinned) and GC will not change this memory.

// ByteArray object has been pinned

GC = GCHandle.Alloc( byArray, GCHandleType.Pinned );

Another interesting point, the conversion of a byte array to string, can be accomplished by using the ASCIIEncoding class.

//Convert ByteArray to String

public string GetStringStoredInByteArray( int iID )
{
    return( ( new ASCIIEncoding()).GetString( ( ( ByteArray ) 
      byarArray[ iID ] ).GetByteArray() ) ).TrimEnd( new char [] { 
      Convert.ToChar( ( int ) 0 ) } );
}

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here