For a while at my company we've used a home-grown ObjectPool<T>
implementation that provides blocking access to its contents. It's pretty straightforward: a Queue<T>
, an object
to lock on, and an AutoResetEvent
to signal to a "borrowing" thread when an item is added.
The meat of the class is really these two methods:
public T Borrow() { lock (_queueLock) { if (_queue.Count > 0) return _queue.Dequeue(); } _objectAvailableEvent.WaitOne(); return Borrow(); } public void Return(T obj) { lock (_queueLock) { _queue.Enqueue(obj); } _objectAvailableEvent.Set(); }
We have been using this and a few other collection classes instead of those provided by System.Collections.Concurrent
because we are using .NET 3.5, not 4.0. But recently we discovered that since we are using Reactive Extensions, we actually do have the Concurrent
namespace available to us (in System.Threading.dll).
Naturally, I figured that since BlockingCollection<T>
is one of the core classes in the Concurrent
namespace, it would probably offer better performance than anything I or my teammates wrote.
So I tried writing a new implementation that works very simply:
public T Borrow() { return _blockingCollection.Take(); } public void Return(T obj) { _blockingCollection.Add(obj); }
To my surprise, according to some simple tests (borrowing/returning to the pool a few thousand times from multiple threads), our original implementation significantly beats BlockingCollection<T>
in terms of performance. They both appear to work correctly; it's just that our original implementation seems to be much faster.
My question:
BlockingCollection<T>
offers greater flexibility (I understand it works by wrapping an IProducerConsumerCollection<T>
), which necessarily introduces performance overhead?BlockingCollection<T>
class?BlockingCollection<T>
, am I just not using properly? For example, is the Take
/Add
approach overly simplistic, and there's a far better-performing way to get the same functionality?Unless anyone has some insight to offer in response to that third question, it looks like we'll be sticking with our original implementation for now.
BlockingCollection<T> is a thread-safe collection class that provides the following features: An implementation of the Producer-Consumer pattern. Concurrent adding and taking of items from multiple threads.
That being said, BlockingCollection<T> works upon any IProducerConsumerCollection<T> (specified in the constructor). If you don't provide one in the constructor, internally, it will use a ConcurrentQueue<T> . This causes it to be FIFO, since it will actually be (internally) a queue.
While for ConcurrentQueue you do not need to specifically lock as the to Enqueue/Dequeue would have the necessary item level locks. "Simple queue" is usually the problem.
BlockingCollection can be used as the collection class. Producer thread generates the data and consumer thread is consuming the data. We set the maximum limit of collection class. Producer cannot add new objects more than maximum limit and consumer cannot remove data from an empty collection class.
There are a couple of potential possibilities, here.
First, BlockingCollection<T>
in the Reactive Extensions is a backport, and not exactly the same as the .NET 4 final version. I wouldn't be surprised if the performance of this backport differs from .NET 4 RTM (though I haven't profiled this collection, specifically). Much of the TPL performs better in .NET 4 than in the .NET 3.5 backport.
That being said, I'd suspect your implementation will out-perform BlockingCollection<T>
if you have a single producer thread and a single consumer thread. With one producer and one consumer, your lock is going to have a smaller impact on the total performance, and the reset event is a very effective means of waiting on the consumer side.
However, BlockingCollection<T>
is designed to allow many producer threads to "enqueue" data very well. This will not perform well with your implementation, as the locking contention will start to become problematic fairly quickly.
That being said, I'd also like to point out one misconception here:
...it would probably offer better performance than anything I or my teammates wrote.
This is often not true. The framework collection classes typically perform very well, but are often not the most performant option for a given scenario. That being said, they tend to perform well while being very flexible and very robust. They often tend to scale very well. "Home-written" collection classes often outperform framework collections in specific scenarios, but tend to be problematic when used in scenarios outside of the one for which they were specifically designed. I suspect this is one of those situations.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With