【问题标题】:How might a class like .NET's ConcurrentBag<T> be implemented?如何实现像 .NET 的 ConcurrentBag<T> 这样的类?
【发布时间】:2010-12-13 22:00:45
【问题描述】:

我发现自己对即将到来的 .NET 4.0 框架中存在 ConcurrentBag&lt;T&gt; 类非常感兴趣:

当订购无关紧要时,包对于存储对象很有用,而且与集合不同,包支持重复。

我的问题是:如何实现这个想法?我熟悉的大多数集合基本上相当于(在引擎盖下)某种形式的数组,其中的顺序可能并不“重要”,但有一个顺序(这就是为什么,即使它没有不需要,枚举几乎总是会经过一个未更改的集合,无论是ListQueueStack 等,都以相同的顺序)。

如果我不得不猜测,我可能会建议在内部它可能是Dictionary&lt;T, LinkedList&lt;T&gt;&gt;;但考虑到仅使用 any 类型 T 作为键是没有意义的,这实际上似乎很可疑。

我期待/希望的是,这实际上是一种已经在某处“弄清楚”的既定对象类型,知道这种既定类型的人可以告诉我。这对我来说太不寻常了——这些概念在现实生活中很容易理解,但作为开发人员很难转化为可用的类——这就是为什么我对这些可能性感到好奇。

编辑

一些响应者建议Bag 可能是内部哈希表的一种形式。这也是我最初的想法,但我预见到这个想法有两个问题:

  1. 当您没有适合相关类型的哈希码函数时,哈希表就不是那么有用了。
  2. 仅在集合中跟踪对象的“计数”与存储对象不同。

正如 Meta-Knight 所建议的,也许举个例子可以更清楚地说明这一点:

public class ExpensiveObject() {
    private ExpensiveObject() {
        // very intense operations happening in here
    }

    public ExpensiveObject CreateExpensiveObject() {
        return new ExpensiveObject();
    }
}

static void Main() {
    var expensiveObjects = new ConcurrentBag<ExpensiveObject>();

    for (int i = 0; i < 5; i++) {
        expensiveObjects.Add(ExpensiveObject.CreateExpensiveObject());
    }

    // after this point in the code, I want to believe I have 5 new
    // expensive objects in my collection

    while (expensiveObjects.Count > 0) {
        ExpensiveObject expObj = null;
        bool objectTaken = expensiveObjects.TryTake(out expObj);
        if (objectTaken) {
            // here I THINK I am queueing a particular operation to be
            // executed on 5 separate threads for 5 separate objects,
            // but if ConcurrentBag is a hashtable then I've just received
            // the object 5 times and so I am working on the same object
            // from 5 threads at the same time!
            ThreadPool.QueueUserWorkItem(DoWorkOnExpensiveObject, expObj);
        } else {
            break;
        }
    }
}

static void DoWorkOnExpensiveObject(object obj) {
    ExpensiveObject expObj = obj as ExpensiveObject;
    if (expObj != null) {
        // some work to be done
    }
}

【问题讨论】:

  • +1 因为很高兴知道这个类的存在
  • Dan-o:您在示例代码中的 5 行注释毫无意义。当然,此时你的包里有 5 个独立的对象。[public ExpensiveObject CreateExpensiveObject()] 中的“new”运算符保证了这一点。
  • @Boogaloo:看看 Meta-Knight 和 flyfishr64 的回复。他们建议 Bag 可以实现为 HashTable,其中对象作为键,值设置为关联键的出现次数。 如果是这种情况,那么“添加”一个对象与将该值(出现次数)加一相同,“删除”一个对象将返回该对象并简单地减少该值价值。你是对的,CreateExpensiveObject 会保证一个对象是创建的,但不能保证它被添加到Bag如果实现是一个哈希表。
  • 嗯..我的错误。我过去没有使用过哈希。我假设默认的哈希生成器会为每个对象创建一个唯一的哈希值——你可以用自己的哈希生成器覆盖它。别管我。 :)

标签: .net language-agnostic collections bag


【解决方案1】:

如果你查看ConcurrentBag&lt;T&gt;的详细信息,你会发现它在内部基本上是一个自定义的链表。

由于 Bags 可以包含重复项,并且不能通过索引访问,因此双向链表是一个非常好的实现选择。这允许锁定非常细粒度以进行插入和删除(您不必锁定整个集合,只需锁定您要插入/删除的节点周围的节点)。由于您不担心重复,因此不涉及散列。这使得双链表变得完美。

【讨论】:

  • 好点:我什至没有想到一个包不需要做任何匹配的事实——它只需要对象并返回它们。对我来说似乎是一个真正令人困惑的问题突然变得不那么令人费解了。
  • 由于其他一些响应者有不同的答案(尽管您的答案对我来说最有意义),我很想知道您在哪里找到这些细节?此外,只需锁定正在发生插入/删除的节点,这是一个很好的观点......我很想知道,ConcurrentQueue&lt;T&gt; 实际上在内部也是一个LinkedList&lt;T&gt; 吗? (否则,入队/出队似乎会不必要地昂贵)。
  • 您可以在 .NET 4 beta 2 中的 System.dll 上使用 Reflector - 您将看到它的完整实现。它实际上不是 LinkedList,而是使用 ConcurrentBag.Node 的内部 ConcurrentBag.ThreadLocalList - 但它基本上是一个自定义的双链表。
  • 另外,补充一点,据我所知,实现为每个线程保留不同的集合,并且如果当前线程集合为空,则仅从另一个线程“窃取”一个项目。这种“窃取”需要线程同步,这会带来自己的性能损失。因此,如果 Add 的线程也是 Take 的线程,则 ConcurrentBag 最有效,以最大限度地减少性能损失。
  • 我想知道为什么它是双链表,我可以从这里看到 referencesource.microsoft.com/#system/sys/system/collections/… 双重仅用于在列表的开头轻松添加节点,也用于“偷”
【解决方案2】:

由于排序无关紧要,因此 ConcurrentBag 可以在后台使用哈希表来快速检索数据。但与Hashset 不同的是,包接受重复项。也许每个项目都可以与 Count 属性配对,该属性在添加项目时设置为 1。如果您第二次添加相同的项目,您可以只增加该项目的 Count 属性。

然后,要删除计数大于 1 的项目,您只需减少该项目的计数即可。如果计数为 1,您将从哈希表中删除 Item-Count 对。

【讨论】:

  • 听起来你和我有相似的想法,但请考虑一下:首先,这会将ConcurrentBag&lt;T&gt; 的使用限制为适合用作键的类型。其次,如果您只是在Hashset 中的每个条目上都有一个Count 属性,那么该对象并不是真正的in 包,我觉得这基本上违背了目的。 (所以,如果我的ConcurrentBag&lt;Thing&gt; 中有10 个相同Thing 的副本,并且我调用TryTake 10 次,第一次之后返回的是什么?相同的Thing?然后我想我有10 个对象但是真的,我只有 1 个。)
  • 如果两个项目在 ConcurrentBag 中被视为重复项,那么它们不一样吗?如果它们相同,如果您多次调用 TryTake 会得到完全相同的对象,难道不是预期的吗?我不确定 ConcurrentBag 如何与“不适合用作键的类型”一起工作......
  • 如果你有一个具体的例子,它会帮助我想象你提到的问题;-)
  • @Meta-Knight:我在我的问题中添加了一个非常详细的示例。让我知道你的想法。
【解决方案3】:

嗯,在 smalltalk 中(Bag 的概念来源于此),集合与散列基本相同,尽管它允许重复。它不是存储重复的对象,而是维护一个“出现计数”,例如,每个对象的引用计数。如果 ConcurrentBag 是一个忠实的实现,这应该给你一个起点。

【讨论】:

    【解决方案4】:

    我相信“Bag”的概念与“Multiset”是同义词。

    如果您对它们的实现方式感兴趣,有许多“Bag”/“Multiset”实现(这些恰好是 java)是开源的。

    这些实现表明,“Bag”可以根据您的需要以多种方式实现。例子有TreeMultiset、HashMultiset、LinkedHashMultiset、ConcurrentHashMultiset。

    谷歌收藏
    Google 有许多 "MultiSet" implementations,其中一个是 ConcurrentHashMultiset。

    Apache Commons
    Apache 有许多“Bag”实现。

    【讨论】:

      【解决方案5】:

      这里有一些关于 ConcurrentBag 的好信息:http://geekswithblogs.net/BlackRabbitCoder/archive/2011/03/03/c.net-little-wonders-concurrentbag-and-blockingcollection.aspx

      ConcurrentBag 的工作方式 是利用新的 ThreadLocal 类型(新增 System.Threading for .NET 4.0) 以便 每个使用包的线程都有一个列表 本地的只是那个线程。

      这意味着添加或删除 线程本地列表要求非常低 同步。问题来了 线程去哪里消费一个项目 但它的本地列表是空的。在这个 如果包执行“偷工减料” 它会从另一个地方抢走一件物品 列表中有项目的线程。 这需要更高的水平 同步增加了一点 take 操作的开销。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-10-24
        • 2011-09-20
        • 2014-01-21
        • 1970-01-01
        • 2016-06-27
        • 1970-01-01
        相关资源
        最近更新 更多