处理 .NET 应用程序中的泄漏(事件类型)

Posted

技术标签:

【中文标题】处理 .NET 应用程序中的泄漏(事件类型)【英文标题】:Handles leak (Event type) in .NET application 【发布时间】:2014-03-15 22:56:58 【问题描述】:

我有一个用 .NET 4.0 编写的 Windows 窗体应用程序。最近,在执行一些测试时,我注意到句柄存在一些问题。下表显示了结果:

如您所见,唯一增加的句柄类型是Event

所以我的问题是:所描述的问题是否可能是由 Windows 窗体应用程序引起的?我的意思是,我不使用AutoResetEventManualResetEvent 同步线程。我确实使用线程,但是从上面的表格中可以看出线程句柄的数量似乎还可以。那么,我假设它们被 CLR 很好地管理了?

这可能是由我也在我的应用中使用的任何第三方组件引起的吗?

如果某事不清楚,我会尝试回答您的问题。感谢您的帮助!

【问题讨论】:

如果您使用的是 Process Explorer,您可以在下方窗格中看到所有事件句柄的列表(Ctrl+L 是我机器上的快捷键)。它们的名称可能会帮助您识别它们的创建位置。除此之外,您还可以使用windbg 来调查它们的来源。 【参考方案1】:

这个答案有点晚了,但我只是在调查我的一些代码中的一个非常相似的问题时遇到了这个问题,并通过在 CreateEvent 的反汇编中的系统调用处放置一个断点找到了答案。希望其他人会发现此答案很有用,即使对于您的特定用例来说为时已晚。

答案是,当存在争用时,.NET 会为各种线程原语创建事件内核对象。值得注意的是,我已经制作了一个测试应用程序,可以显示它们是在使用“lock”语句时创建的,不过,大概任何 Slim 线程原语都会执行类似的延迟创建。

重要的是要注意句柄没有泄漏,尽管越来越多的句柄可能表明代码中的其他地方存在泄漏。当垃圾收集器收集创建它们的对象(例如,lock 语句中提供的对象)时,句柄将被释放。

我在下面粘贴了我的测试代码,它将小规模展示泄漏(我的测试机器上大约有 100 个泄漏的事件句柄 - 你的里程可能会有所不同)。

一些具体的兴趣点:

一旦列表被清除并运行GC.Collect(),所有创建的句柄都将被清除。

将 ThreadCount 设置为 1 将阻止创建任何事件句柄。

同样,注释掉lock 语句将导致不会创建句柄。

index 的计算中删除 ThreadCount(第 72 行)将大大减少争用,从而阻止创建几乎所有句柄。

无论您让它运行多长时间,它都不会创建超过 200 个句柄(.NET 似乎出于某种原因为每个对象创建 2 个)。

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

【讨论】:

【参考方案2】:

事件是 .Net 中内存泄漏的主要来源,AutoResetEventManualResetEvent 的名称​​非常错误。他们不是原因。

当你看到这样的东西时:

myForm.OnClicked += Form_ClickHandler

这就是我们所说的事件类型。当您注册事件处理程序时,事件源(如OnClicked)会保留对处理程序的引用。如果您创建和注册新的处理程序,您必须取消注册该事件(例如 myForm.OnClicked -= Form_ClickHandler),否则您的内存使用量将持续增长。

更多信息:

Why and How to avoid Event Handler memory leaks? C# Events Memory Leak

【讨论】:

我怀疑 OP 的表是指托管事件,但更可能是系统事件句柄(即使用 CreateEvent 创建的)。列名表示“句柄类型”,可能是从外部应用程序(进程浏览器或其他东西)输出的。 好点 Groo。 @rwasik - 你能澄清一下表格代表什么和/或你是如何生成它的吗?你知道你是否在拨打非托管电话吗? 表代表句柄,表示(根据定义)进程的对象表中对象句柄的数量。我使用 Process Explorer 来获取它。在看到事件类型属性后,您可以看到描述并显示“同步对象”。更重要的是,“事件信息”部分包含两个信息:类型 - 同步,信号 - 假。这就是为什么我认为它与线程而不是事件处理程序有关。不,我不会拨打任何非托管电话。 @rwasik:你能试试定期(即每 10 分钟)做GC.Collect(4, GCCollectionMode.Forced); GC.WaitForPendingFinalizers();。如果这阻止了事件句柄计数的增长,您可能遇到了一个已知的 .Net 错误。 我试过你的方法。经过 16 小时的测试,您建议的 GC 函数被调用了 10 次。它没有帮助,因为事件句柄的数量已经从 264 增加到 9400。这是否意味着外部 dll 中有问题? “一个已知的 .Net 错误”是什么意思?

以上是关于处理 .NET 应用程序中的泄漏(事件类型)的主要内容,如果未能解决你的问题,请参考以下文章

Xamarin.Forms UWP 中的内存泄漏

防止 WPF 中的内存泄漏

Electron App 中的事件发射器内存泄漏

添加触摸事件时iOS webview中的内存泄漏

如何检测 ASP.net 应用程序中的 SqlServer 连接泄漏?

。net应用程序在特定计算机中的内存泄漏