ThreadLocal<>和内存泄漏

8
.Net 4中,ThreadLocal<>实现了IDisposable接口。但是似乎调用Dispose()并没有真正释放对于持有的线程本地对象的引用。
以下代码复现了这个问题:
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;

namespace ConsoleApplication2
{
    class Program
    {
        class ThreadLocalData
        {
            // Allocate object in LOH
            public int[] data = new int[10 * 1024 * 1024];
        };

        static void Main(string[] args)
        {
            // Stores references to all thread local object that have been created
            var threadLocalInstances = new List<ThreadLocalData>();
            ThreadLocal<ThreadLocalData> threadLocal = new ThreadLocal<ThreadLocalData>(() =>
            {
                var ret = new ThreadLocalData();
                lock (threadLocalInstances)
                    threadLocalInstances.Add(ret);
                return ret;
            });
            // Do some multithreaded stuff
            int sum = Enumerable.Range(0, 100).AsParallel().Select(
                i => threadLocal.Value.data.Sum() + i).Sum();
            Console.WriteLine("Sum: {0}", sum);
            Console.WriteLine("Thread local instances: {0}", threadLocalInstances.Count);

            // Do our best to release ThreadLocal<> object
            threadLocal.Dispose();
            threadLocal = null;

            Console.Write("Press R to release memory blocks manually or another key to proceed: ");
            if (char.ToUpper(Console.ReadKey().KeyChar) == 'R')
            {
                foreach (var i in threadLocalInstances)
                    i.data = null;
            }
            // Make sure we don't keep the references to LOH objects
            threadLocalInstances = null;
            Console.WriteLine();

            // Collect the garbage
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();

            Console.WriteLine("Garbage collected. Open Task Manager to see memory consumption.");
            Console.Write("Press any key to exit.");
            Console.ReadKey();
        }
    }
}

线程本地数据存储引用了一个大对象。如果没有手动将引用设置为null,GC不会回收这些大对象。我使用任务管理器观察内存消耗情况,并运行内存分析器。在垃圾回收之后,我创建了一个快照。分析器显示泄漏的对象由GCHandle根引用,并在此处分配:

mscorlib!System.Threading.ThreadLocal<T>.GenericHolder<U,V,W>.get_Boxed()
mscorlib!System.Threading.ThreadLocal<T>.get_Value()
ConsoleApplication2!ConsoleApplication2.Program.<>c__DisplayClass3.<Main>b__2( int ) Program.cs

ThreadLocal<>设计中似乎存在缺陷。将所有分配的对象存储以进行进一步清理的技巧很丑陋。有什么解决方法吗?


1
你现在是在调试模式还是发布模式下?此外,任务管理器并不是用来衡量你正在测量的东西非常实用。 - Marc Gravell
最好使用 GC.GetTotalMemory(true) 来测量内存,但这也不能保证所有东西都会被回收。 - Ray
1
打印GC.GetTotalMemory()。当我不将“data”字段清零时,它会返回335607644;而当我将其清零时,则返回63268。 - SergeyS
刚试了一下Release和Debug模式,没有明显的区别。 - SergeyS
1
@SergeyS 你应该更新你的问题,使用 GC.GetTotalMemory,这样人们就不会因为任务管理器方面而被吓到了。 - Scott Chamberlain
Sergey,你需要理解CLR内存管理和垃圾回收的基本原理。你所提出的问题和期望并没有太多意义。Dispose不同于确定性回收;回收不同于释放资源,也不同于将内存返回给操作系统。 - Mahol25
2个回答

1

内存可能已经被垃圾回收了,但CLR进程仍未释放它。它倾向于保留分配的内存一段时间,以防它稍后需要使用它,这样就不必进行昂贵的内存分配。


2
如果将“data”字段清零,GC的行为会有所不同。此外,内存分析器显示ThreadLocalData对象实际上是从ThreadLocal<>内部某处根源化的。 - SergeyS

0

在 .Net 4.5 DP 上运行时,我在您的应用程序中按下 R 键和不按键之间没有任何区别。如果在 4.0 中实际上存在内存泄漏问题,那么似乎已经被修复了。

(4.5 是一个原地更新,所以我无法在同一台计算机上测试 4.0,抱歉。)


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