.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?
A ThreadLocal variable is referenced by its thread and its lifecycle is bound to it. In most application servers, threads are reused via thread pools and are never garbage collected. If the application code does not carefully clear the ThreadLocal variable, it results in a nasty memory leak.
Thread Safety With the exception of Dispose(), all public and protected members of ThreadLocal<T> are thread-safe and may be used concurrently from multiple threads.
You should always call remove because ThreadLocal class puts values from the Thread Class defined by ThreadLocal. Values localValues; This will also cause to hold reference of Thread and associated objects. the value will be set to null and the underlying entry will still be present.
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.
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.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With