This answer is a bit late, but I just ran across the question while investigating a very similar issue in some of my code and found the answer by placing a break point at the syscall in the disassembly of CreateEvent. Hopefully other people will find this answer useful, even if it is too late for your specific use case.
The answer is that .NET creates Event kernel objects for various threading primitives when there is contention. Notably, I have made a test application that can show they are created when using the "lock" statement, though, presumably, any of the Slim threading primitives will perform similar lazy creation.
It is important to note that the handles are NOT leaked, though an increasing number may indicate a leak elsewhere in your code. The handles will be released when the garbage collector collects the object that created them (eg, the object provided in the lock statement).
I have pasted my test code below which will showcase the leak on a small scale (around 100 leaked Event handles on my test machine - your mileage may vary).
A few specific points of interest:
Once the list is cleared and the GC.Collect()
is run, any created handles will be cleaned up.
Setting ThreadCount to 1 will prevent any Event handles from being created.
Similarly, commenting out the lock
statement will cause no handles to be created.
Removing the ThreadCount
from the calculation of index
(line 72) will drastically reduce contention and thus prevent nearly all the handles from being created.
No matter how long you let it run for, it will never create more than 200 handles (.NET seems to create 2 per object for some reason).
using System.Collections.Generic;
using System.Threading;
namespace Dummy.Net
{
public static class Program
{
private static readonly int ObjectCount = 100;
private static readonly int ThreadCount = System.Environment.ProcessorCount - 1;
private static readonly List<object> _objects = new List<object>(ObjectCount);
private static readonly List<Thread> _threads = new List<Thread>(ThreadCount);
private static int _currentIndex = 0;
private static volatile bool _finished = false;
private static readonly ManualResetEventSlim _ready = new ManualResetEventSlim(false, 1024);
public static void Main(string[] args)
{
for (int i = 0; i < ObjectCount; ++i)
{
_objects.Add(new object());
}
for (int i = 0; i < ThreadCount; ++i)
{
var thread = new Thread(ThreadMain);
thread.Name = $"Thread {i}";
thread.Start();
_threads.Add(thread);
}
System.Console.WriteLine("Ready.");
Thread.Sleep(10000);
_ready.Set();
System.Console.WriteLine("Started.");
Thread.Sleep(10000);
_finished = true;
foreach (var thread in _threads)
{
thread.Join();
}
System.Console.WriteLine("Finished.");
Thread.Sleep(3000);
System.Console.WriteLine("Collecting.");
_objects.Clear();
System.GC.Collect();
Thread.Sleep(3000);
System.Console.WriteLine("Collected.");
Thread.Sleep(3000);
}
private static void ThreadMain()
{
_ready.Wait();
while (!_finished)
{
int rawIndex = Interlocked.Increment(ref _currentIndex);
int index = (rawIndex / ThreadCount) % ObjectCount;
bool sleep = rawIndex % ThreadCount == 0;
if (!sleep)
{
Thread.Sleep(10);
}
object obj = _objects[index];
lock (obj)
{
if (sleep)
{
Thread.Sleep(250);
}
}
}
}
}
}