Search code examples
c#collectionsconcurrencyconcurrent-collections

Why doesn't ConcurrentBag<T> implement ICollection<T>?


I have a method which takes an IList<T> and adds stuff to it. I would like to pass it a ConcurrentBag<T> in some cases, but it doesn't implement IList<T> or ICollection<T>, only the non-generic ICollection, which doesn't have an Add method.

Now, I see why it can't (maybe) implement IList<T> - it's not an ordered collection so it won't make sense for it to have an indexer. But I don't see an issue with any of the ICollection<T> methods.

So, why? And, also - is there a thread-safe collection in .NET that does implement more robust interfaces?


Solution

  • Why doesn't ConcurrentBag<T> implement ICollection<T>?

    Because it can't. Specifically the functionality of the method ICollection<T>.Remove is not supported by the ConcurrentBag<T>. You can't remove a specific item from this collection. You can only "take" an item, and it's up to the collection itself to decide which item to give you.

    The ConcurrentBag<T> is a specialized collection intended to support specific scenarios (mixed producer-consumer scenarios, mainly object pools). Its internal structure was chosen to support optimally these scenarios. The ConcurrentBag<T> maintains internally one WorkStealingQueue (internal class) per thread. Items are always pushed in the tail of the current thread's queue. Items are popped from the tail of the current thread's queue, unless its empty, in which case an item is "stolen" from the head of another thread's queue. Pushing and popping from the local queue is lock-free. That's what this collection was designed to do best: to store and retrieve items from a local buffer, without contending for locks with other threads. Writing lock-free code like this is extremely hard. If you see the source code of this class, it will blow your mind. Could this core functionality stay lock-free if another thread was allowed to steal an item from any place in the WorkStealingQueue, not just the head? I don't know the answer to this, but if I had to guess, based on the following comment in the WorkStealingQueue.TryLocalPeek method I'd say no:

    // It is possible to enable lock-free peeks, following the same general approach
    // that's used in TryLocalPop.  However, peeks are more complicated as we can't
    // do the same kind of index reservation that's done in TryLocalPop; doing so could
    // end up making a steal think that no item is available, even when one is. To do
    // it correctly, then, we'd need to add spinning to TrySteal in case of a concurrent
    // peek happening. With a lock, the common case (no contention with steals) will
    // effectively only incur two interlocked operations (entering/exiting the lock) instead
    // of one (setting Peek as the _currentOp).  Combined with Peeks on a bag being rare,
    // for now we'll use the simpler/safer code.
    

    So the TryPeek uses a lock, not because making it lock-free is impossible but because it is hard. Imagine how harder it would be if items could be removed from arbitrary places inside the queue. And the Remove functionality would require exactly that.