为什么 ConcurrentBag 不支持实现 ICollection

发布于 2024-10-31 04:32:09 字数 771 浏览 2 评论 0 原文

我有一个方法,它采用 IList 并向其中添加内容。我想传递一个 ConcurrentBag 在某些情况下,但它不实现 IListICollection,仅实现非通用 ICollection,它没有 Add 方法。

现在,我明白为什么它不能(也许)实现 IList - 它不是一个有序集合,因此它拥有索引器是没有意义的。但我没有发现任何 ICollection 方法。

那么,为什么呢?而且,.NET 中是否有线程安全的集合可以实现更健壮的接口?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

五里雾 2024-11-07 04:32:09

List 不是并发的,因此它可以实现 ICollection,它为您提供了一对方法 Contains添加。如果 Contains 返回 false,您可以安全地调用 Add,知道它会成功。

ConcurrentBag 是并发的,因此它无法实现 ICollection,因为 Contains 返回的答案在您调用时可能无效添加。相反,它实现了 IProducerConsumerCollection,它提供了单一方法 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");
    }
}

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");
    }
}
三生殊途 2024-11-07 04:32:09

SynchronizedCollection,实现了 IList< T>ICollection 以及 IEnumerable

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

小…楫夜泊 2024-11-07 04:32:09

为什么 ConcurrentBag 不实现 ICollection

因为它不能。具体来说,该方法的功能 ConcurrentBag 不支持 >ICollection.Remove。您无法从此集合中删除特定项目。您只能“采取” 一个项目,由集合本身决定 哪一项给你。

ConcurrentBag 是一个专门的集合,旨在支持特定场景(混合生产者-消费者场景,主要是 对象池)。其内部结构的选择是为了最佳地支持这些场景。 ConcurrentBag 在内部维护一个 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.

所以 < a href="https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentbag-1.trypeek" rel="nofollow noreferrer">TryPeek< /a> 使用,不是因为使其无锁是不可能的,而是因为它很难。想象一下,如果可以从队列中的任意位置删除项目会有多困难。 Remove 功能正是需要这样的功能。

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.

愿得七秒忆 2024-11-07 04:32:09

...

using System.Linq;


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

提供某种 ICollection 功能。

...

using System.Linq;


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

Giving a sorts of ICollection capability.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文