Easy Efficiency: Unleashing a Thread-Safe Object Pool in C#

Iede Snoek
November 19, 2023 0 Comment

Introduction

Sometimes, it’s useful to have a collection of pre-initialized objects ready for efficiency. This is especially handy when dealing with resource-intensive tasks, such as creating database connections. In this article, I’ll explain three implementations of a thread-safe object pool in C#:

What you do is that make a pool of objects, usually an array, and you have one object managing that array, giving out objects, and returning, basically doing the bookkeeping.

In this article I will explain 3 implementations of a threadsafe object pool in C#:

  1. Basic Object-pool: Manages a list of available and active objects.
  2. Queryable Object-pool: Allows specifying properties the object must have when requesting.
  3. Dynamic Object Pool: Dynamically adds objects using a creation function when the pool is empty.

    The PoolModel

    Before diving into implementations, let’s discuss the PoolModel class. It enables automatic return of objects to the pool when they are no longer needed. Here’s a simple breakdown:

    public class PoolModel<T>:IDisposable
    {
        private T value;
        private IObjectPool<T> pool;
    
        /// <summary>
        /// Constructor for the pool model
        /// </summary>
        /// <param name="value">The value to be wrapped</param>
        /// <param name="pool">The object pool to which this PoolModel belongs</param>
        public PoolModel(T value,IObjectPool<T> pool)
        {
            this.value = value;
            this.pool = pool;
        }
    
        /// <summary>
        /// Unwraps the value
        /// </summary>
        /// <returns>The value</returns>
        public T Unwrap()
        {
            return this.value;
        }
    
        /// <summary>
        /// Returns the poolmodel to the pool
        /// </summary>
        public void Dispose()
        {
            this.pool.ReturnObject(this);
        }
    }
    

    Basically the PoolModel holds two things:

    1. The object we get from the pool
    2. A reference to the pool, which we will need in the Dispose() method to return it to the pool

    The Unwrap() method simply gets the wrapped value from the model.

    The Basic Object-pool

    The basic object-pool maintains two lists (available and active objects) and a lock object for critical sections. Objects are obtained and returned within a lock to ensure thread safety.

            /// <summary>
            /// A list of available objects
            /// </summary>
            protected List<T> availableObjects;
            /// <summary>
            /// A list of active objects
            /// </summary>
            protected List<T> activeObjects;
    
            /// <summary>
            /// Simple lock object
            /// </summary>
            protected object lockObject = new();

    Getting objects

    In the GetObject()method, we check whether we have any objects left, if not we throw an exception.

    If we have an object available, we update the lists and return the object. Note that the updates of the list as well finding out whether an object is available are considered critical, and are therefore within the lock statement.

    public virtual PoolModel<T> GetObject()
    {
        T obj;
    
        lock (lockObject)
        {
            if (this.availableObjects.Count == 0)
            {
                throw new NoObjectsInPoolException("No objects available");
            }
            obj = this.availableObjects[0];
            this.availableObjects.RemoveAt(0);
            this.activeObjects.Add(obj);
        }
        return new PoolModel<T>(obj, this);
    
    }

    Returning or releasing objects

    If we are done with an object we need to return it to the pool of available objects. That is what is done in the ReturnObject() method:

    public void ReturnObject(PoolModel<T> obj)
    {
        lock (lockObject)
        {
            var unwrapped = obj.Unwrap();
            if (!this.activeObjects.Contains(unwrapped))
            {
                throw new NoObjectsInPoolException("Object not in pool");
            }
            this.activeObjects.Remove(unwrapped);
            this.availableObjects.Add(unwrapped);
        }
    }

    Note that we depend that we return exactly the same object we got from the pool. Also updating the pools is done in a lock context.

    Example use:

    Here is a simple example of the use of this object pool:

    var initialObjects=new List<int>{1,2,3};
    
    var objectPool=new ObjectPool<int>(initialObjects);
    
    using (var model = objectPool.GetObject())
    {
        var value = model.Unwrap();
        Console.WriteLine($"Value is {value}");
    }
    

    Here we are using the using statement to make sure that we return the object after its use, and we keep the object pool up to date.

    The Queryable object-pool

    This pool inherits from the basic object-pool and adds a method allowing specification of properties an object must have.

    public PoolModel<T> GetObject(Func<T, bool> query)
    {
    
        lock(lockObject)
        {
            var obj = this.availableObjects.FirstOrDefault(query);
            if (obj == null)
            {
                throw new NoObjectsInPoolException("No objects matching the query available");
            }
            this.availableObjects.Remove(obj);
            this.activeObjects.Add(obj);
    
            return new PoolModel<T>(obj, this);
        }
        
    }

    The only difference with the GetObject() method in the base class is the addition of a query function Func<T,bool>. In the method we use that query the availableObject fields. If that returns null we throw an exceptions, otherwise we update the pools.

    Example use:

    Here is an example use:

    var initialObjects=new List<int>{1,2,3};
    
    var objectPool=new QueryableObjectPool<int>(initialObjects);
    
    using (var model = objectPool.GetObject(x=>x>2))
    {
        var value = model.Unwrap();
        Console.WriteLine($"Value is {value}");
    }

    Make sure you catch the exception thrown when an object can not be found.

    Dynamic object pool

    This pool allows adding objects dynamically using a factory function.

            public DynamicObjectPool(Func<T> factory) : base(new List<T>())
            {
                this.factory = factory;
            }

    And the GetObject() method looks like this:

    public override PoolModel<T> GetObject()
    {
        T? obj;
    
        lock (lockObject)
        {
            if (this.availableObjects.Count == 0)
            {
                obj= this.factory?.Invoke();
                if (obj == null)
                {
                    throw new NoObjectsInPoolException("No objects available");
                }
                availableObjects.Add(obj);
    
            }
            obj = this.availableObjects[0];
            this.availableObjects.RemoveAt(0);
            this.activeObjects.Add(obj);
        }
        return new PoolModel<T>(obj, this);
    }

    The main difference is the call to the factory function if no objects are available. Keep in mind that that function can fail to, hence the extra test.

    Example use

    Let’s see how we can use this object pool. It is a rather contrived example:

    var initialObjects = new List<Example> { new Example("one"), new Example("two"), new Example("three") };
    
    var objectPool=new DynamicObjectPool<Example>(()=>new Example("Created"),initialObjects);
    
    for (int i = 0; i < 5; i++)
    {
        var model = objectPool.GetObject();
        var value = model.Unwrap();
    
        Console.WriteLine($"Value is {value}");
    }
    
    class Example
    {
        public Example(string name)
        {
            this.Name = name;
        }
        private string Name { get; }
    
        public override string ToString()
        {
            return $"Name is {this.Name}";
        }
    }
    
    

    Run this, and you will see the first three objects printed out, however that last two will print out ‘Created’ which shows that the factory method has been called.

    One enhancement I will be working on is to make the factory method a bit more flexible.

    Conclusion

    In conclusion, object-pooling is a flexible pattern in C#. The provided NuGet package and source code offer a starting point, with possible enhancements and bug fixes encouraged. The mentioned package can be found on NuGet, and the source code is available on GitHub.

    You can find the Nuget package here: https://www.nuget.org/packages/EsoxSolutions.ObjectPool

    The source is available here: https://github.com/snoekiede/EsoxSolutions.ObjectPool