Handles leak (Event type) in .NET application
Asked Answered
D

2

8

I have a Windows Forms application written in .NET 4.0. Recently, while execution some tests, I noticed that there is some problem with handles. Table below shows the results:

enter image description here

As you can see the, only handle type which is increasing is Event.

So my question is: Is it possible that the described problem is caused by a Windows Forms application? I mean, I do not synchronize threads using AutoResetEvent or ManualResetEvent. I do use threads, but what can be seen from the table above the number of thread handles seems to be ok. So, I assume that they are well managed by CLR?

Can it be caused by any third party components I am also using in my app?

If sth is unclear I will try to answer your questions. Thanks for help!

Degression answered 16/2, 2014 at 18:36 Comment(1)
If you're using Process Explorer, you can see the list of all event handles in the lower pane (Ctrl+L is the shortcut on my machine). Their names might help you identify where they are created. Apart from that, you can also use windbg to investigate their origin.Compressor
I
4

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);
                    }
                }
            }
        }
    } 
}
I answered 24/4, 2020 at 22:48 Comment(0)
G
2

Events are the main source of memory leaks in .Net, and AutoResetEvent and ManualResetEvent are very badly named. They are not the cause.

When you see something like this:

myForm.OnClicked += Form_ClickHandler

That is the type of event this is talking about. When you register an event handler, the event source (like OnClicked) keeps a reference to the handler. If you create and register new handlers you MUST unregister the event (like myForm.OnClicked -= Form_ClickHandler) otherwise your memory use will keep growing.

For more info:

Guile answered 16/2, 2014 at 21:8 Comment(8)
I doubt that OP's table is referring to managed events, but more likely to system event handles (i.e. created using CreateEvent). The column name says "Handle type" and is probably output from an external app (process explorer or something).Compressor
Good point Groo. @Degression -- could you clarify what the table is representing and/or how you generated it? Do you know if you are making unmanaged calls?Guile
Table is representing handles, which means (according to definition) the number of object handles in the process's object table. I used Process Explorer to get it. After seeing the Event type properties you can see description and it says 'A synchronization object'. What's more there is a section 'Event Info' containing two information: Type - Synchronization, Signaled - False. That's why I think it is connected with threads rather than event handlers. And no I don't make any unmanaged calls.Degression
@rwasik: can you try doing GC.Collect(4, GCCollectionMode.Forced); GC.WaitForPendingFinalizers(); Periodically (i.e. every 10 minutes). If that stops the event handle count growing, you may have run into a known .Net bug.Guile
I tried your approach. After 16h of testing, GC functions you suggested were invoked 10 times. It didn't help because number of event handles has grown from 264 to 9400. Does it mean some problem in external dll? And what do you mean by "a known .Net bug"?Degression
If you quickly open and close many threads in a high-memory use app, there may be a long delay is collecting thread resources. It doesn't sound like this is happening. Can you list the external dlls you are referencing?Guile
Sorry I can't. But after those tests is it still possible that sth is wrong in .net application?Degression
I can't tell without more information. Perhaps try disabling parts of your system and see when the problem goes away. This can be difficult with tightly coupled code, but is one of the best ways of hunting down difficult bugs.Guile

© 2022 - 2024 — McMap. All rights reserved.