【问题标题】:Is there a memory leak in the ConcurrentBag<T> implementation? [duplicate]ConcurrentBag<T> 实现中是否存在内存泄漏? [复制]
【发布时间】:2012-08-14 13:24:53
【问题描述】:

可能重复:
Possible memoryleak in ConcurrentBag?

编辑1:

实际的问题是。你能确认一下,还是我的样本有误,我遗漏了一些明显的东西?

我认为 ConcurrentBag 是无序列表的简单替代品。但是我错了。 ConcurrentBag 确实将自身作为 ThreadLocal 添加到创建线程,这基本上会导致内存泄漏。

   class Program
    {
        static void Main(string[] args)
        {
            var start = GC.GetTotalMemory(true);
            new Program().Start(args);
            Console.WriteLine("Diff: {0:N0} bytes", GC.GetTotalMemory(true) - start);
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
            Thread.Sleep(5000);
        }

        private void Start(string[] args)
        {
            for (int i = 0; i < 1000; i++)
            { 
                var bag = new ConcurrentBag<byte>();
                bag.Add(1);
                byte by;
                while (bag.TryTake(out by)) ;
            }
        }

我可以根据我添加到包中的数据量来制作 250 KB 或 100 GB 的 Diff。数据和包都消失了。

当我用 Windbg 闯入这个并且我做了一个 !DumpHeap 类型并发

....

000007ff00046858        1           24 System.Threading.ThreadLocal`1+GenericHolder`3[[System.Collections.Concurrent.ConcurrentBag`1+ThreadLocalList[[System.Byte, mscorlib]], System],[System.Threading.ThreadLocal`1+C0[[System.Collections.Concurrent.ConcurrentBag`1+ThreadLocalList[[System.Byte, mscorlib]], System]], mscorlib],[System.Threading.ThreadLocal`1+C0[[System.Collections.Concurrent.ConcurrentBag`1+ThreadLocalList[[System.Byte, mscorlib]], System]], mscorlib],[System.Threading.ThreadLocal`1+C0[[System.Collections.Concurrent.ConcurrentBag`1+ThreadLocalList[[System.Byte, mscorlib]], System]], mscorlib]]
000007feed812648        2           64 System.Collections.Concurrent.ConcurrentStack`1[[System.Int32, mscorlib]]
000007feece41528        1          112 System.Collections.Concurrent.CDSCollectionETWBCLProvider
000007ff000469e0     1000        32000 System.Threading.ThreadLocal`1+Boxed[[System.Collections.Concurrent.ConcurrentBag`1+ThreadLocalList[[System.Byte, mscorlib]], System]]
000007feed815900     1000        32000 System.Collections.Concurrent.ConcurrentStack`1+Node[[System.Int32, mscorlib]]
000007ff00045530     1000        72000 System.Collections.Concurrent.ConcurrentBag`1+ThreadLocalList[[System.Byte, mscorlib]]

当我创建一个空的 ConcurrentBag 让一些工作线程向它添加数据时,ConcurrentBag 并且只要创建线程仍然存在,它的数据就会在那里。

这样我得到了几 GB 的内存泄漏。我确实通过使用列表和锁来“修复”这个问题。 ConcurrentBag 可能很快,但作为具有相同对象生命周期的 List 的简单替换,它是无用的。

如果我在主线程上创建了一个 ConcurrentBag,只要线程还活着,我就会保留它。这不是我所期望的,它可能会导致严重的痛苦。

【问题讨论】:

  • 这是一个问题还是一个陈述?
  • 我在问这是真的还是我确实错过了一些明显的东西。我会添加这个。
  • 我在 LINQPad 构建的最后一个稳定框架上运行了您的代码,包含 100000 袋 1000 个项目,我的差异是负数或零...
  • @VirtualBlackFox:您应该使用额外的进程和调试器来确定发生了什么。负值表示您不应该使用 LINQPad。

标签: c# .net task-parallel-library base-class-library


【解决方案1】:

你说得对,ConcurrentBag 创建了一个 ThreadLocal 副本,实际上它们针对同一个线程正在读取和写入数据包的场景进行了优化:“...... ConcurrentBag 是一个线程安全的包实现,针对场景进行了优化同一个线程将同时生产和消费存储在包中的数据。”

另一方面,我没有看到这里有什么奇怪的行为;线程存在,并发包存在。当线程完成 GC 就会完成它的工作。

【讨论】:

  • 问题是内存似乎一直保持活动状态,直到线程退出。如果它是一个长时间运行的线程怎么办?
  • 我认为 ConcurrentBag 适合 TPL。但是,如果我无法控制线程的生命周期,我就无法确定何时回收内存。这是一个相当有限的因素。
  • 我会将此标记为答案,尽管我确实认为 ConcurrentBag 的局部变量不应该分配一个新的 TLS 实例而没有释放它的方法。如果您在本地方法中创建 100 万个 ConcurrentBag 实例,您将保留它们,并且您无法对包调用 dispose,因为它没有实现 IDisposable。
  • @Alois Kraus:ThreadLocal 有一个终结器,所以当 ConcurrentBag 上没有剩余引用时,线程静态将被清除。最好在 ConcurrentBag 上公开 Dispose,但至少它没有泄漏。
  • 您已在 .NET 4.5 上检查过这一点。对于 .NET 4.0,有一个泄漏确实会一直留在那里,直到持有它的线程终止。 .NET 4.5 变得更好了,但你仍然无法明确控制你的内存,这很遗憾。包中物品的数据结构也失去了大部分功能。我仍然想念 .NET 中没有泄漏或终结问题的无序无锁集合。
【解决方案2】:

来自文档

ConcurrentBag 是一种线程安全的包实现,针对同一线程将同时生产和使用包中存储的数据的场景进行了优化。

来自When to use a thread-safe collection

在混合的生产者-消费者场景中,对于大型和小型工作负载,ConcurrentBag 通常比任何其他并发收集类型更快且更具可扩展性。

我想说您对 ConcurrentBag 的假设是不正确的。首先,它不会将其自身添加到 ThreadLocal,它使用线程本地存储为访问它的每个线程提供单独的内部列表。它不仅仅是一个线程安全的无序列表。

一旦您意识到包使用 TLS,您认为内存泄漏实际上是预期的行为 - 只要线程在使用中,就不需要清除数据。

说了这么多,我自己直到现在才意识到 ConcurrentBag 的额外功能。

我在“What is ConcurrentBag”中找到了关于 ConcurrentBag 如何使用单独列表及其在不同场景中的方法成本的非常好的描述。我真希望这个描述出现在 MSDN 文档中。

就我个人而言,既然我知道 ConcurrentBag 的特殊行为,我将开始更多地使用它。

更新:

刚刚检查了 Ayende 的 this post“ConcurrentBag 使用的 ThreadLocal 没想到会有很多实例。这个问题已经修复,现在可以运行得相当快”

【讨论】:

  • 这并没有解决问题:如何在不终止所有相关线程的情况下释放内存(想想线程池线程)?
  • 没有问题。这就是类(和 TLS)的工作方式。顺便说一句,这里提出并回答了完全相同的问题stackoverflow.com/questions/5353164/…
  • 第一个链接不起作用。第二个链接显示了一个非常简化的视图。它没有解决我遇到的任何问题。例如。如果您创建一袋整数,则每个项目不使用 4 个字节,而是 32 个字节(节点类实例(x64 上为 20 个字节),上一个指针(8 个字节 x64),下一个指针(8 个字节 x64),T 项 4 个字节) .因此,对于海量数据的快速并发访问对于此类来说是没有问题的,因为开销非常高。
  • 糟糕,第一个链接末尾有一个句号。固定的。至于这些问题,我认为您正在尝试使课程适应从未创建过的场景。为高效的发布者/消费者处理而创建的类永远不会像带锁的简单数组那样高效。另一方面,无锁容器可以扩展到比锁更多的核心。事实上,TPL 通常不是为超高效的大规模处理而设计的——这就是 SSE2 操作的用途,而 .NET 没有它们。 Mono 有,我非常想念他们。
  • post by Ayende 的评论告诉我们:“.NET 4.5 仍然没有解决这个问题”
【解决方案3】:

为什么不在第二个 GC.Collect() 之后移动 Console.WriteLine?否则,您可能会看到比您预期更多的对象。

您也可以尝试将 Main 中的所有内容放入一个循环中以获取一些统计信息。即使您不移动您的写入,您也可能会在之后看到更小的增量。

干杯!

【讨论】:

  • 列表已被过滤。列出的分配仅归因于 ConcurrentBag 分配。
猜你喜欢
  • 1970-01-01
  • 2011-07-18
  • 2012-02-20
  • 2016-08-16
  • 2011-05-16
  • 2011-06-26
  • 2011-10-07
  • 2011-01-29
  • 2011-05-08
相关资源
最近更新 更多