The Pooling Design Pattern in C#

The Pooling Design Pattern in C#

·

5 min read

The pooling pattern is a software design pattern that is used to manage the creation and reuse of objects, often in situations where creating new objects is expensive or time-consuming. The basic idea behind the pattern is to create a pool of objects that can be reused, rather than creating new objects each time they are needed.

Implementation

To implement the pooling pattern in C#, you will need to create a pool class that manages the creation and reuse of objects. Here is an example implementation of a pool class that can be used to manage the creation and reuse of objects of type T:

class ObjectPool<T>
{
    private readonly Stack<T> _pool;
    private readonly Func<T> _objectGenerator;

    public ObjectPool(Func<T> objectGenerator)
    {
        _pool = new Stack<T>();
        _objectGenerator = objectGenerator;
    }

    public T GetObject()
    {
        if (_pool.Count > 0)
        {
            return _pool.Pop();
        }
        return _objectGenerator();
    }

    public void PutObject(T obj)
    {
        _pool.Push(obj);
    }
}

The ObjectPool class uses a Stack to store the available objects, and a Func<T> to create new objects when none are available in the pool. The GetObject method is used to retrieve an object from the pool, either by returning an existing object or by creating a new one using the _objectGenerator function. The PutObject method is used to return an object to the pool.

Example

Here is an example of how to use the ObjectPool class to manage the creation and reuse of StringBuilder objects:

class StringBuilderPool : ObjectPool<StringBuilder>
{
    public StringBuilderPool() : base(() => new StringBuilder()) { }
}
var pool = new StringBuilderPool();

var sb = pool.GetObject();
sb.Append("Hello, ");
sb.Append("world!");
Console.WriteLine(sb.ToString()); // output "Hello, world!"

pool.PutObject(sb);

sb = pool.GetObject();
Console.WriteLine(sb.ToString()); // output ""

You can see how the StringBuilderPool class is created by inheriting the ObjectPool<StringBuilder> and passing the new StringBuilder() as a generator. Next we can get the object of StringBuilder by calling GetObject() method of the pool and can release it back to the pool by calling PutObject(sb).

Real-World Example

A real-world example of using the pooling pattern is in the management of database connections. When a new database connection is needed, a connection is retrieved from a pool of already created and idle connections, instead of creating a new one. This improves performance and scalability as creating new connections is typically a time-consuming and resource-intensive operation.

Here is an example of how to use the pooling pattern to manage database connections in C# using the popular ADO.NET library and the ObjectPool class:

class DbConnectionPool : ObjectPool<DbConnection>
{
    private readonly string _connectionString;

    public DbConnectionPool(string connectionString) : base(() =>
    {
        var connection = new SqlConnection(_connectionString);
        connection.Open();
        return connection;
    })
    {
        _connectionString = connectionString;
    }

    public override DbConnection GetObject()
    {
        var connection = base.GetObject();
        if (connection.State != ConnectionState.Open)
        {
            connection.Open();
        }
        return connection;
    }

    public override void PutObject(DbConnection connection)
    {
        if (connection.State != ConnectionState.Closed)
        {
            connection.Close();
        }
        base.PutObject(connection);
    }
}

This DbConnectionPool class is inherited from the ObjectPool<DbConnection> and uses a Func<DbConnection> that creates a new connection by opening a new SqlConnection with the provided connection string. The GetObject method is overridden to open the connection if it is closed, and the PutObject method is overridden to close the connection before returning it to the pool. This allows the pool to ensure that all connections are open when they are handed out and closed when they are returned, ensuring that they are ready to be used and release resources.

You can now create an instance of the DbConnectionPool class and use it to retrieve and release connections as needed, for example:

var connectionString = "Data Source=localhost;Initial Catalog=myDB;Integrated Security=True;";
var pool = new DbConnectionPool(connectionString);

using (var connection = pool.GetObject())
{
    // Use the connection to execute a command
    var command = connection.CreateCommand();
    command.CommandText = "SELECT * FROM Customers";
    using (var reader = command.ExecuteReader())
    {
        // Process the results
    }
}

When the using block is exited, the connection variable is disposed and the PutObject method is called, returning the connection to the pool.

When to Use it?

The pooling pattern should be used in situations where creating new objects is expensive or time-consuming, and a large number of objects will be needed over time. Here are some specific scenarios where the pooling pattern can be useful:

  • When creating new objects is a resource-intensive operation, such as establishing a new database connection or opening a new network socket. In these cases, using a pool to reuse existing objects can improve performance and scalability.

  • When a large number of objects will be needed for a short period of time, such as during a spike in traffic to a website. Using a pool can help avoid the overhead of creating and destroying a large number of objects.

  • When the number of objects needed exceeds the capacity of the system to create them, such as when there are limits on the number of database connections that can be established or when memory is limited.

  • When the lifetime of the objects is relatively long compared to the time they spend idle. In this case, pooling the objects can significantly reduce the number of objects that need to be created, reducing memory usage and garbage collection overhead.

Important

The pooling pattern is not always the best option, it depends on the specific scenario and the trade-offs involved. If the objects are cheap and short-lived, creating new instances on demand may be more efficient than maintaining a pool of objects. It's also worth considering the cost of creating a pooling infrastructure, if it's too complex and costly, it might not worth it. Therefore it's important to evaluate the specific requirements and constraints of the system and weigh the benefits of pooling against the costs of implementation and maintenance.