8

.Net 4. ThreadLocal<> implements IDisposable. But it seems that calling Dispose() doesn't actually release references to thread local objects being held.

This code reproduces the problem:

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

Thread local data stores a reference to a large object. GC doesn't collect these large objects if references are not nulled manually. I used Task Manager to observe memory consumption. I also run memory profiler. I made a snapshot after garbage was collected. The profiler showed that leaked object is rooted by GCHandle and was allocated with here:

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

That seems to be a flaw in ThreadLocal<> design. The trick with storing all allocated objects for further cleanup is ugly. Any ideas on how to work around that?

Nick Bastin
  • 30,415
  • 7
  • 59
  • 78
SergeyS
  • 3,909
  • 2
  • 15
  • 17
  • 1
    Are you in debug or release for this? also, task manager is not really very usable for what you are measuring – Marc Gravell Sep 27 '11 at 09:49
  • You're better off using `GC.GetTotalMemory(true)` to measure the memory, but that also doesn't guarantee that everything will be collected. – Ray Sep 27 '11 at 09:52
  • 1
    Printed GC.GetTotalMemory(). It gives 335607644 when I don't zero **data** field and 63268 when I do. – SergeyS Sep 27 '11 at 09:56
  • Just tried both Relase and Debug. No significant difference. – SergeyS Sep 27 '11 at 09:56
  • 1
    @SergeyS you should update your question with the new findings and useing GC.GetTotalMemory so people will not be turned away by the task manager aspect. – Scott Chamberlain Oct 06 '11 at 03:19
  • Sergey, you need to understand the fundamentals of how CLR memory magagement and how garbage collection works. What you are asking and expecting does not make much sense. Dispose is not the same as deterministic collection. Collection is not the same as deallocation, neither is it the same as returning memory to the operating system. – Mahol25 May 15 '12 at 09:46

2 Answers2

1

The memory has probably been garbage collected but the CLR process hasn't let go of it yet. It tends to hold onto allocated memory for a bit in case it needs it later so it doesn't have to do an expensive memory allocation.

Deleted
  • 4,804
  • 1
  • 22
  • 17
  • 2
    GC behaves differently if 'data' field is zeroed. Plus Memory Profiler shows that ThreadLocalData object is actually rooted somewhere from ThreadLocal<> internals. – SergeyS Sep 27 '11 at 09:52
0

Running on .Net 4.5 DP, I don't see any difference between pressing R or not in your application. If there actually was a memory leak in 4.0, it seems it was fixed.

(4.5 is a in-place update, so I can't test 4.0 on the same computer, sorry.)

svick
  • 236,525
  • 50
  • 385
  • 514