为什么ConcurrentBag<T>没有实现ICollection<T>接口?

30

我有一个方法,它接受一个 IList<T> 并向其中添加内容。在某些情况下,我想将其传递给一个ConcurrentBag<T>,但它没有实现IList<T>ICollection<T>,只有非泛型的ICollection,它没有Add方法。

现在,我明白了 (也许) 为什么它不能实现IList<T> - 因为它不是一个有序集合,所以对于它有索引器就没有意义了。但我不明白为什么它不能实现任何ICollection<T>方法。

那么,为什么呢?还有,在.NET中有哪些线程安全的集合可以实现更强大的接口?


1
显然,他们不想实现ICollection<>.Remove(),只想要TryTake()。我看到的最大区别是,Remove()只能以线程安全的方式使对象消失。而TryTake()是原子性的。 - Hans Passant
2
感谢介绍我使用 System.Collections.Concurrent 命名空间。 - William Mioch
4个回答

36

List<T>不是并发的,因此它可以实现ICollection<T>,这给你一对方法ContainsAdd。如果Contains返回false,那么你可以安全地调用Add,因为它将成功。

ConcurrentBag<T>是并发的,因此它不能实现ICollection<T>,因为在调用Add之前,Contains返回的答案可能已经无效了。相反,它实现了IProducerConsumerCollection<T>,提供单个方法TryAdd,该方法执行ContainsAdd两个方法的工作。

因此,不幸的是,你希望操作的两个集合都没有共同的接口。有许多方法可以解决这个问题,但当API非常相似时,我首选的方法是为两个接口提供方法重载,然后使用lambda表达式创建代表,使用它们自己的方法执行相同的操作。然后你可以使用这个代表来替代你几乎要执行的公共操作。

这里有一个简单的例子:

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");
    }
}

3
“ConcurrentBag<T>实现了非泛型的ICollection接口,这个事实是否会影响到你的论点?” - Biscuits
1
ConcurrentBag.TryAdd总是返回true - 可以在此处查看:https://referencesource.microsoft.com/#System/sys/system/collections/concurrent/ConcurrentBag.cs,185 - 因此上面示例中的Process函数永远不会抛出异常。 - matra

6

1
我可以使用它,但它依赖于锁定,因此不够可扩展,是吗? - Doron Yaacoby
@Doron - 或许吧,但它似乎是针对同一线程的读/写进行了优化。您可能需要查看此问题的回答:https://dev59.com/Cm445IYBdhLWcg3wl7bW - tvanfosson

2
为什么 ConcurrentBag<T> 没有实现 ICollection<T>
因为它无法实现。ConcurrentBag<T> 不支持 ICollection<T>.Remove 方法的功能。您无法从此集合中删除特定项。您只能"取出"一个项目,由集合本身决定给您哪个项目

ConcurrentBag<T>是一种专门用于支持特定场景(主要是混合生产者-消费者场景和 对象池)的集合。它的内部结构被选择为最大程度地支持这些场景。每个线程在ConcurrentBag<T>中保持一个WorkStealingQueue(内部类)。项目总是被推到当前线程队列的尾部。项目从当前线程的队列尾部弹出,除非它为空,在这种情况下,将从另一个线程的队列头部“偷取”一个项目。从本地队列中推送和弹出是无锁的。这就是这个集合最擅长的事情:将项目存储和检索到本地缓冲区,而不与其他线程争夺锁。编写这样的无锁代码非常困难。如果您看到这个类的源代码,它会让您大吃一惊。如果允许另一个线程从WorkStealingQueue任何位置窃取项目,而不仅仅是从头开始,那么这个核心功能能否保持无锁状态呢?我不知道答案,但如果我必须猜测,基于WorkStealingQueue.TryLocalPeek方法中以下评论,我会说不行:

// 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.

因此,TryPeek 使用了 lock,不是因为使其无锁化是不可能的,而是因为它很难实现。想象一下如果可以从队列中的任意位置删除项目会更加困难。Remove 功能正需要实现这一点。

Remove 的功能目前不受 ConcurrentBag<T> 的公共 API 直接支持,但这并不意味着它“不能”被完成。理论上看,作者们至少可以选择实现 ICollection<T> 并实现一个 Remove 方法。你的答案本质上是一种循环论证,即“它没有实现 ICollection<T> 是因为它没有实现 ICollection<T>”。 - Bradley Grainger
@BradleyGrainger 我编辑了答案。希望现在解释更好了。 - Theodor Zoulias

-2

...

using System.Linq;


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

提供一种 ICollection 能力的排序。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接