This is the fifth article in my mini series about object pooling. Be sure to read part 1, part 2, part 3 and part 4 first. This part is going to make some modifications to the code in the GetObject method to implement various alternative behaviours there. Think about it: what do you want to happen if there’s no free object to be had in the pool? I came up with the following possible options:

  1. Return null. In this case, the caller would have to deal with the problem further if it can’t get a pool object.
  2. Throw an exception. Really just a variant of (1), because it also pushes the problem out to the caller.
  3. Try again. Because objects are being used and returned to the pool all the time, it might make sense just to wait a bit and try to allocate an object one more time… or two more times, even. A maximum number of tries would be good, I guess.
  4. Extend the pool. The general growing/shrinking discussion hasn’t taken place yet in these articles, but possibly the case where no more objects are available would be a good place to grow the pool.

Consider this changed code for the GetObject method:

public T GetObject( ) {
  int tryNo = 1;

  while(tryNo <= maxGetObjectTries) {
    lock (poolLock) {
      Slot unusedSlot = pool.Find(delegate(Slot slot) {
        return !slot.InUse;
      });
      if (unusedSlot != null) {
        unusedSlot.Occupy( );
        return unusedSlot.PoolObject;
      }
    }

    switch (outOfObjectsBehaviour) {
      case OutOfObjectsBehaviour.ReturnNull:
        return default(T);
      case OutOfObjectsBehaviour.ThrowException:
        throw new OutOfObjectsException( );
      case OutOfObjectsBehaviour.WaitAndRetry:
        Thread.Sleep(retryWaitTime);
        break;
      case OutOfObjectsBehaviour.ExtendPool:
        ExtendPool( );
        break;
    }
  }

  throw new OutOfObjectsException( );
}

As you can see, the whole getting-an-object algorithm is potentially repeated multiple times now, possibly sleeping or trying pool extension in between. Finally, when a specific maximum number of retries has run out, an exception is thrown. I’ve specifically decided to do this instead of returning null — I’m assuming there might be no special code to handle null returns on the caller side, otherwise the pool could have been configured to return null in the first place. The next interesting thing is the ExtendPool method, which looks like this:

/// <summary>
/// Extends the pool by a number of objects that's specified by the PoolExtensionBatchSize
/// and the PoolExtensionBatchSizeIsPercent properties.
/// </summary>
void ExtendPool( ) {
  lock (poolLock) {
    int newObjects = poolExtensionBatchSizeIsPercent ?
      pool.Count * poolExtensionBatchSize / 100 : poolExtensionBatchSize;
    ExtendPoolBy(newObjects);
  }
}

Several properties have been added to the pool to support the new functionality and there’s also an enum for the out of objects behaviour. If you’ve been following the articles closely, you may have noticed that the maxPoolSize had already crept in to the ExtendPoolBy method last time, although I hadn’t introduced it yet :-) Here it is now:

class Pool {
  // ...
  private int maxPoolSize;
  /// <summary>
  /// Gets or sets a value indicating the maximum number of objects that the pool
  /// shall hold. The ExtendPoolBy() method won't extend the pool to a larger
  /// size than this.
  /// </summary>
  public int MaxPoolSize {
    get {
      return maxPoolSize;
    }
    set {
      if (maxPoolSize != value) {
        maxPoolSize = value;
        lock (poolLock) {
          // This might be replaced by a call to the proper shrinking
          // method later.
          if (pool.Count > maxPoolSize)
            pool.RemoveRange(maxPoolSize, pool.Count - maxPoolSize);
        }
      }
    }
  }

  private int maxGetObjectTries;
  /// <summary>
  /// Gets or sets a value that defines the maximum tries that are made
  /// to acquire an object from the pool in the GetObject() method.
  /// </summary>
  public int MaxGetObjectTries {
    get {
      return maxGetObjectTries;
    }
    set {
      if (maxGetObjectTries != value) {
        maxGetObjectTries = value;
      }
    }
  }

  private int poolExtensionBatchSize;
  /// <summary>
  /// Gets or sets a value that defines the size of the batch when the pool is
  ///  growing. This can be an absolute value or a percentage, which is
  /// defined by the PoolExtensionBatchSizeIsPercent property.
  /// </summary>
  public int PoolExtensionBatchSize {
    get {
      return poolExtensionBatchSize;
    }
    set {
      if (poolExtensionBatchSize != value) {
        poolExtensionBatchSize = value;
      }
    }
  }

  private bool poolExtensionBatchSizeIsPercent;
  /// <summary>
  /// Gets or sets a flag that indicates whether the value given by
  /// the PoolExtensionBatchSize property is an absolute value or a percentage.
  /// </summary>
  public bool PoolExtensionBatchSizeIsPercent {
    get {
      return poolExtensionBatchSizeIsPercent;
    }
    set {
      if (poolExtensionBatchSizeIsPercent != value) {
        poolExtensionBatchSizeIsPercent = value;
      }
    }
  }

  private int retryWaitTime;
  /// <summary>
  /// Gets or sets a value that defines the time to wait between tries
  /// to acquire a pool object in the GetObject() method. This is only
  /// evaluated for OutOfObjectsBehaviour == WaitAndRetry.
  /// </summary>
  public int RetryWaitTime {
    get {
      return retryWaitTime;
    }
    set {
      if (retryWaitTime != value) {
        retryWaitTime = value;
      }
    }
  }

  private OutOfObjectsBehaviour outOfObjectsBehaviour;
  /// <summary>
  /// Gets or sets a value indicating the behaviour that the GetObject()
  /// method shows when a pool object isn't (immediately) available.
  /// </summary>
  public OutOfObjectsBehaviour OutOfObjectsBehaviour {
    get {
      return outOfObjectsBehaviour;
    }
    set {
      if (outOfObjectsBehaviour != value) {
        outOfObjectsBehaviour = value;
      }
    }
  }
  // ...
}

/// <summary>
/// Defines the possible behaviours that the GetObject()
/// method can shows when a pool object isn't (immediately) available.
/// </summary>
public enum OutOfObjectsBehaviour {
  /// <summary>
  /// Return null or the default value of the type.
  /// </summary>
  ReturnNull,
  /// <summary>
  /// Wait for a time specified by the Pool.RetryWaitTime property and try again.
  /// A maximum number of tries is specified by the Pool.MaxGetObjectTries
  /// property.
  /// </summary>
  WaitAndRetry,
  /// <summary>
  /// Throw an OutOfObjectsException.
  /// </summary>
  ThrowException,
  /// <summary>
  /// Extend the pool by a default batch as defined by the
  /// Pool.PoolExtensionBatchSize and Pool.PoolExtensionBatchSizeIsPercent
  /// properties, then try again. A maximum number of tries is specified by
  /// the Pool.MaxGetObjectTries property.
  /// </summary>
  ExtendPool
}

Sorry I still didn’t make it to the growing/shrinking, at least not completely. Well, next time!