The days of using IDisposable for things like this only came about because of the lack of functional programming concepts in C#. Now that Action and Func<T> exist there's absolutely no reason to use IDisposable for this situation.
A pool of connections/resources can expose a much nicer and simpler interface that removes the need for the using statement altogether and as a result avoids the possibility of the caller "forgetting" to give the resource back to the pool. Just because something implements IDisposable it doesn't mean that the caller will dispose of it properly.
A few points on your implementation:
- Your
BaseClass is completely unnecessary and can be removed.
- The manager class has a poor implementation of what's already in the .NET framework:
ConcurrentQueue. It's not only a better option because you don't have to write it, but it's lock-free and much more efficient.
- The manager class's lock forces serialised access to one thread at a time, even if there are plenty of resources available. If you have 30 connections, you should let 30 threads access your resource pool at once, not just 1. Use a
Sempahore instead.
So with this in mind I'd like to make a couple of suggestions.
- Generalise your resource pool. The resource pool shouldn't be coded to a specific type of thing. Anything of any type can be pooled.
- Avoid the use of disposable and use higher order functions to wrap up the lifetime of the resource. The consumer can not possible forget to give the resource back to the pool because you're doing it all for them.
- Use proper .NET collections to solve the problem, not your own implementation.
- Avoid a straight lock which serialises access when you can instead use a semaphore.
- Don't push the logic of resource creation into the pool. Pass in a function that can do the creation for you, then you can have something that can create objects of arbitrary complexity.
Here's a quick implementation I've just thrown together (haven't compiled it, it's for reference and for ideas only)
public class Pool<TResource>
{
private const int PoolWaitTimeoutMs = 1000;
private readonly ConcurrentQueue<TResource> _resources;
private readonly Semaphore _semaphore;
public Pool(int size, Func<TResource> producer)
{
_resources = new ConcurrentQueue<TResource>();
for(var i = 0; i < size; ++i)
{
_resources.Enqueue(producer());
}
_sempahore = new Semaphore(size, size);
}
public void Consume(Action<TResource> consumer)
{
// This is a helper for when consumers don't produce a result
Consume(new Func<TResource, object>(res => { consumer(res); return null; }));
}
public TResult Consume(Func<TResource, TResult> consumer)
{
// wait for a resource to become available
if (!_semaphore.WaitOne(PoolWaitTimeoutMs))
{
throw new Exception("Unable to acquire resource");
}
TResource res;
try
{
// we should have a resource available given that
// the semaphore says so
if(_resources.TryDequeue(out res))
{
// invoke the consumer function and return the result
return consumer(res);
}
throw new Exception("Queue was empty when it shouldn't be");
}
finally
{
// put it back in the queue if we managed to get one
if(res != null)
{
_resources.Enqueue(res);
}
// release the semaphore we had acquired
_semaphore.Release();
}
}
}
Using the pool them becomes as simple as this:
public class SomePooledResource
{
public void DoStuff() { ... };
public List<string> GetStuff() { ... };
// other stuff
}
// create a pool of 10 resources
var pool = new Pool<SomePooledResource>(10, () => new SomePooledResource);
// use the resource without returning something
pool.Consume(res => res.DoStuff());
// use the resource returning a result
var results1 = pool.Consume(res => res.GetStuff());
// use the resource to do both
var results2 = pool.Consume(res =>
{
res.DoStuff();
return res.GetStuff();
});
I hope that helps. Best of luck.
EDIT -- after Almaz's comment
I was incorrect in my assumption that BlockingCollection<T> wasn't FIFO by default. Now that I've been educated (thanks Almaz!) it's clear that the code above could be better written as follows:
public class Pool<TResource>
{
private const int PoolWaitTimeoutMs = 1000;
private readonly BlockingCollection<TResource> _resources;
public Pool(int size, Func<TResource> producer)
{
_resources = new BlockingCollection<TResource>();
for(var i = 0; i < size; ++i)
{
_resources.Add(producer());
}
}
public void Consume(Action<TResource> consumer)
{
// This is a helper for when consumers don't produce a result
Consume(new Func<TResource, object>(res => { consumer(res); return null; }));
}
public TResult Consume(Func<TResource, TResult> consumer)
{
TResource res;
try
{
// wait for a resource to become available
if (!_resource.TryTake(out res, PoolWaitTimeoutMs))
{
throw new Exception("Unable to acquire resource");
}
// invoke the consumer function and return the result
return consumer(res);
}
finally
{
// put it back in the queue if we managed to get one
if(res != null)
{
_resources.Add(res);
}
}
}
}
Again, haven't compiled this. Just use as a ref. Cheers.
internal bool IsDisposed { get; private set; }. – Leonid Apr 1 '12 at 2:13