Why doesn't ConcurrentBag<T> implement ICollection<T>?
Asked Answered
Q

4

30

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?

Quelpart answered 10/4, 2011 at 12:42 Comment(3)
Clearly they didn't want to implement ICollection<>.Remove(), only TryTake(). The biggest difference I see is that Remove() can only make an object disappear in a thread-safe way. TryTake() is atomic.Sternforemost
Kudos for introducing me to the System.Collections.Concurrent namespace.Robbie
possible duplicate of Why do Queue(T) and Stack(T) not implement ICollection(T)?Rictus
T
2

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.

Tinworks answered 2/2, 2023 at 17:48 Comment(2)
The functionality of Remove isn't currently supported directly by the public API of ConcurrentBag<T>, but that doesn't prove it "can't" be done. It at least seems theoretically possible that the authors could have chosen to implement ICollection<T> and implemented a Remove method. Your answer is essentially a tautology that "it doesn't implement ICollection<T> because it doesn't implement ICollection<T>."Guava
@BradleyGrainger I edited the answer. Hopefully now the explanation is better.Tinworks
S
36

A List<T> is not concurrent and so it can implement ICollection<T> which gives you the pair of methods Contains and Add. If Contains returns false you can safely call Add knowing it will succeed.

A ConcurrentBag<T> is concurrent and so it cannot implement ICollection<T> because the answer Contains returns might be invalid by the time you call Add. Instead it implements IProducerConsumerCollection<T> which provides the single method TryAdd that does the work of both Contains and Add.

So unfortunately you desire to operate on two things that are both collections but don't share a common interface. There are many ways to solve this problem but my preferred approach when the API is as similar as these are is to provide method overloads for both interfaces and then use lambda expressions to craft delegates that perform the same operation for each interface using their own methods. Then you can use that delegate in place of where you would have performed the almost common operation.

Here's a simple example:

public class Processor
{
    /// <summary>
    /// Process a traditional collection.
    /// </summary>
    /// <param name="collection">The collection.</param>
    public void Process(ICollection<string> collection)
    {
        Process(item =>
            {
                if (collection.Contains(item))
                    return false;
                collection.Add(item);
                return true;
            });
    }

    /// <summary>
    /// Process a concurrent collection.
    /// </summary>
    /// <param name="collection">The collection.</param>
    public void Process(IProducerConsumerCollection<string> collection)
    {
        Process(item => collection.TryAdd(item));
    }

    /// <summary>
    /// Common processing.
    /// </summary>
    /// <param name="addFunc">A func to add the item to a collection</param>
    private void Process(Func<string, bool> addFunc)
    {
        var item = "new item";
        if (!addFunc(item))
            throw new InvalidOperationException("duplicate item");
    }
}
Sequel answered 19/4, 2011 at 5:56 Comment(2)
Although, does the fact that ConcurrentBag<T> implement the non-generic ICollection interface not confuse your argument?Monteverdi
ConcurrentBag.TryAdd always returns true - as it can be se here: referencesource.microsoft.com/#System/sys/system/collections/… - so the Process function in example above will never throwMacaw
S
6

There's SynchronizedCollection<T>, implements both IList<T> and ICollection<T> as well as IEnumerable<T>.

Smothers answered 10/4, 2011 at 12:49 Comment(2)
I could use it, but it relies on locks, and therefore less scalable, no?Quelpart
@Doron - perhaps, though it seems to be optimized for read/writes from the same thread. You might want to look at the responses on this question: #4786122Smothers
T
2

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.

Tinworks answered 2/2, 2023 at 17:48 Comment(2)
The functionality of Remove isn't currently supported directly by the public API of ConcurrentBag<T>, but that doesn't prove it "can't" be done. It at least seems theoretically possible that the authors could have chosen to implement ICollection<T> and implemented a Remove method. Your answer is essentially a tautology that "it doesn't implement ICollection<T> because it doesn't implement ICollection<T>."Guava
@BradleyGrainger I edited the answer. Hopefully now the explanation is better.Tinworks
T
-2

...

using System.Linq;


bool result = MyConcurrentBag.Contains("Item");

Giving a sorts of ICollection capability.

Tuberculosis answered 13/3, 2018 at 6:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.