Reputation: 9760
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?
Upvotes: 31
Views: 8951
Reputation: 43545
Why doesn't
ConcurrentBag<T>
implementICollection<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.
Upvotes: 2
Reputation: 2710
...
using System.Linq;
bool result = MyConcurrentBag.Contains("Item");
Giving a sorts of ICollection capability.
Upvotes: -2
Reputation: 34240
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");
}
}
Upvotes: 36
Reputation: 532495
There's SynchronizedCollection<T>
, implements both IList<T>
and ICollection<T>
as well as IEnumerable<T>
.
Upvotes: 6