c# - 如何诊断句柄泄漏源

标签 c# multithreading winforms c#-4.0 memory-leaks

问题
我昨天刚刚添加了一些性能日志记录,因为我注意到很久以前观看任务管理器时出现句柄泄漏,尽管修复它的优先级很低。这是隔夜运行,每 10 秒采样一次。
由于时间限制,我还没有运行它失败,而且我的测试计算机也是我的开发计算机,所以在编写代码时运行它并不理想......所以我不确定它是否/何时会崩溃,但我高度怀疑这只是时间问题。
Graph of application resource usages and performance
注:区域中的红色框是我“停止”工作循环并在短暂暂停后重新启动它的地方。线程在“停止”处从 ~100 下降到 ~20。直到循环从 ~62,000 到 ~40,000 大约 30 秒后重新启动,句柄才下降。所以有些句柄正在被 GC 处理,只是没有我预期的那么多。我无法弄清楚是什么根阻止了所有这些句柄被收集或它们最初来自哪里(即任务、GUI、文件等)。
如果您已经知道可能导致此问题的原因,则无需进一步阅读。我已经提供了此信息的其余部分和代码,以供引用,以一种霰弹枪式的方法来解决问题。随着根本原因缩小,我将删除、编辑等。出于同样的原因,如果缺少感兴趣的东西,请告诉我,我会尽力提供(日志、转储等)。

我做了什么
我自己已经在 Tracking Handle Misuse 上完成了本教程并尽可能查看转储文件以找到句柄打开和关闭的位置......但是成千上万的句柄实在是太多了,我无法让符号加载,所以指针只是胡言乱语对我来说。
我还没有通过我的列表中的以下两个,但想知道是否有一些更友好的方法首先......

  • Debug Leaky Apps: Identify And Prevent Memory Leaks In Managed Code
  • Tracking down managed memory leaks (how to find a GC leak)

  • 我还将我怀疑是导致此问题的潜在原因的代码拆分为另一个小应用程序,并且一切似乎都可以毫无问题地收集垃圾(尽管与实际应用程序相比,执行模式大大简化了)。
    潜在罪魁祸首
    我确实有几个长期存在的实例化类,只要应用程序打开,它们就会持续存在,包括 5 个表单,每个表单只创建一次,然后根据需要隐藏/显示。我使用一个主对象作为我的应用程序 Controller ,然后模型和 View 通过事件以演示者优先模式连接到演示者。
    以下是我在此应用程序中所做的一些事情,这些事情可能重要也可能不重要:
  • 使用自定义 Action , Func和广泛的 lambdas,其中一些可能是长期存在的
  • 3 个用于事件的自定义委托(delegate),可以生成 Task s 用于异步执行。
  • 安全调用扩展 Controls .
  • 非常非常频繁地使用 TaskParallel.For/Parallel.Foreach运行工作方法(或上述事件)
  • 永远不要使用 Thread.Sleep(),而是使用使用 AutoResetEvent 的自定义 Sleep.For()。

  • 主循环
    此应用程序的一般流程是运行 基于对 中的一系列文件的循环离线 中的数字输入信号的版本和轮询在线版本。以下是带有 注释的 sudo 代码离线 这是我可以在不需要外部硬件的情况下从笔记本电脑运行的版本,以及上面的图表监控的内容(我目前无法访问 在线 模式的硬件)。
    public void foo()
    {
        // Sudo Code
        var InfiniteReplay = true;
        var Stopped = new CancellationToken();
        var FileList = new List<string>();
        var AutoMode = new ManualResetEvent(false);
        var CompleteSignal = new ManualResetEvent(false);
        Action<CancellationToken> PauseIfRequired = (tkn) => { };
    
        // Enumerate a Directory...
    
        // ... Load each file and do work
        do
        {
            foreach (var File in FileList)
            {
                /// Method stops the loop waiting on a local AutoResetEvent
                /// if the CompleteSignal returns faster than the
                /// desired working rate of ~2 seconds
                PauseIfRequired(Stopped);
    
                /// While not 'Stopped', poll for Automatic Mode
                /// NOTE: This mimics how the online system polls a digital
                /// input instead of a ManualResetEvent.
                while (!Stopped.IsCancellationRequested)
                {
                    if (AutoMode.WaitOne(100))
                    {
                        /// Class level Field as the Interface did not allow
                        /// for passing the string with the event below
                        m_nextFile = File;
    
                        // Raises Event async using Task.Factory.StartNew() extension
                        m_acquireData.Raise();
                        break;
                    }
                }
    
                // Escape if Canceled
                if (Stopped.IsCancellationRequested)
                    break;
    
                // If In Automatic Mode, Wait for Complete Signal
                if (AutoMode.WaitOne(0))
                {
                    // Ensure Signal Transition
                    CompleteSignal.WaitOne(0);
                    if (!CompleteSignal.WaitOne(10000))
                    {
                        // Log timeout and warn User after 10 seconds, then continue looping
                    }
                }
            }
            // Keep looping through same set of files until 'Stopped' if in Infinite Replay Mode
        } while (!Stopped.IsCancellationRequested && InfiniteReplay);
    }
    
    异步事件
    下面是事件的扩展,大多数是使用默认的异步选项执行的。 'TryRaising()' 扩展只是将委托(delegate)包装在一个 try-catch 中并记录任何异常(虽然它们不会重新抛出,但它不是正常程序流的一部分,它们负责捕获异常)。
    using System.Threading.Tasks;
    using System;
    
    namespace Common.EventDelegates
    {
        public delegate void TriggerEvent();
        public delegate void ValueEvent<T>(T p_value) where T : struct;
        public delegate void ReferenceEvent<T>(T p_reference);
    
        public static partial class DelegateExtensions
        {
            public static void Raise(this TriggerEvent p_response, bool p_synchronized = false)
            {
                if (p_response == null)
                    return;
    
                if (!p_synchronized)
                    Task.Factory.StartNew(() => { p_response.TryRaising(); });
                else
                    p_response.TryRaising();
            }
    
            public static void Broadcast<T>(this ValueEvent<T> p_response, T p_value, bool p_synchronized = false)
                where T : struct
            {
                if (p_response == null)
                    return;
    
                if (!p_synchronized)
                    Task.Factory.StartNew(() => { p_response.TryBroadcasting(p_value); });
                else
                    p_response.TryBroadcasting(p_value);
            }
    
            public static void Send<T>(this ReferenceEvent<T> p_response, T p_reference, bool p_synchronized = false)
                where T : class
            {
                if (p_response == null)
                    return;
    
                if (!p_synchronized)
                    Task.Factory.StartNew(() => { p_response.TrySending(p_reference); });
                else
                    p_response.TrySending(p_reference);
            }
        }
    }
    
    GUI 安全调用
    using System;
    using System.Windows.Forms;
    using Common.FluentValidation;
    using Common.Environment;
    
    namespace Common.Extensions
    {
        public static class InvokeExtensions
        {
            /// <summary>
            /// Execute a method on the control's owning thread.
            /// </summary>
            /// http://stackoverflow.com/q/714666
            public static void SafeInvoke(this Control p_control, Action p_action, bool p_forceSynchronous = false)
            {
                p_control
                    .CannotBeNull("p_control");
    
                if (p_control.InvokeRequired)
                {
                    if (p_forceSynchronous)
                        p_control.Invoke((Action)delegate { SafeInvoke(p_control, p_action, p_forceSynchronous); });
                    else
                        p_control.BeginInvoke((Action)delegate { SafeInvoke(p_control, p_action, p_forceSynchronous); });
                }
                else
                {
                    if (!p_control.IsHandleCreated)
                    {
                        // The user is responsible for ensuring that the control has a valid handle
                        throw
                            new
                                InvalidOperationException("SafeInvoke on \"" + p_control.Name + "\" failed because the control had no handle.");
    
                        /// jwdebug
                        /// Only manually create handles when knowingly on the GUI thread
                        /// Add the line below to generate a handle http://stackoverflow.com/a/3289692/1718702
                        //var h = this.Handle;
                    }
    
                    if (p_control.IsDisposed)
                        throw
                            new
                                ObjectDisposedException("Control is already disposed.");
    
                    p_action.Invoke();
                }
            }
        }
    }
    
    Sleep.For()
    using System.Threading;
    using Common.FluentValidation;
    
    namespace Common.Environment
    {
        public static partial class Sleep
        {
            public static bool For(int p_milliseconds, CancellationToken p_cancelToken = default(CancellationToken))
            {
                // Used as "No-Op" during debug
                if (p_milliseconds == 0)
                    return false;
    
                // Validate
                p_milliseconds
                    .MustBeEqualOrAbove(0, "p_milliseconds");
    
                // Exit immediate if cancelled
                if (p_cancelToken != default(CancellationToken))
                    if (p_cancelToken.IsCancellationRequested)
                        return true;
    
                var SleepTimer =
                    new AutoResetEvent(false);
    
                // Cancellation Callback Action
                if (p_cancelToken != default(CancellationToken))
                    p_cancelToken
                        .Register(() => SleepTimer.Set());
    
                // Block on SleepTimer
                var Canceled = SleepTimer.WaitOne(p_milliseconds);
    
                return Canceled;
            }
        }
    }
    

    最佳答案

    到目前为止所有的评论都非常有帮助,我发现我的 handle 泄漏至少有一个来源是 Sleep.For()方法。我仍然认为我有句柄泄漏,但速度明显变慢,我现在也更好理解 为什么他们正在泄漏。

    它与传入 token 的范围有关,并在 using 语句中清理方法内的本地 token 。一旦我解决了这个问题,我开始看到所有未命名的 Event Process Explorer 中的句柄被创建和销毁,而不仅仅是坐在那里。

    顺便说一句,我发现了 Anatomy of a "Memory Leak"昨晚深夜,肯定会更多地了解 Windbg 以进行进一步调查。

    我还再次进行了长时间运行的性能测试,以查看这是否是唯一的泄漏,并检查了使用 WaitHandles 的代码的其他部分,以确保我正确地确定范围和处置它们。

    固定 Sleep.For()

    using System.Threading;
    using Common.FluentValidation;
    using System;
    
    namespace Common.Environment
    {
        public static partial class Sleep
        {
            /// <summary>
            /// Block the current thread for a specified amount of time.
            /// </summary>
            /// <param name="p_milliseconds">Time to block for.</param>
            /// <param name="p_cancelToken">External token for waking thread early.</param>
            /// <returns>True if sleeping was cancelled before timer expired.</returns>
            public static bool For(int p_milliseconds, CancellationToken p_cancelToken = default(CancellationToken))
            {
                // Used as "No-Op" during debug
                if (p_milliseconds == 0)
                    return false;
    
                // Validate
                p_milliseconds
                    .MustBeEqualOrAbove(0, "p_milliseconds");
    
                // Merge Tokens and block on either
                CancellationToken LocalToken = new CancellationToken();
                using (var SleeperSource = CancellationTokenSource.CreateLinkedTokenSource(LocalToken, p_cancelToken))
                {
                    SleeperSource
                        .Token
                        .WaitHandle
                        .WaitOne(p_milliseconds);
    
                    return SleeperSource.IsCancellationRequested;
                }
            }
        }
    }
    

    测试应用程序(控制台)

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using Common.Environment;
    using System.Threading;
    
    namespace HandleTesting
    {
        class Program
        {
            private static CancellationTokenSource static_cts = new CancellationTokenSource();
    
            static void Main(string[] args)
            {
                //Periodic.StartNew(() =>
                //{
                //    Console.WriteLine(string.Format("CPU_{0} Mem_{1} T_{2} H_{3} GDI_{4} USR_{5}",
                //        Performance.CPU_Percent_Load(),
                //        Performance.PrivateMemorySize64(),
                //        Performance.ThreadCount(),
                //        Performance.HandleCount(),
                //        Performance.GDI_Objects_Count(),
                //        Performance.USER_Objects_Count()));
                //}, 5);
    
                Action RunMethod;
                Console.WriteLine("Program Started...\r\n");
                var MainScope_cts = new CancellationTokenSource();
                do
                {
                    GC.Collect();
                    GC.WaitForPendingFinalizers();
                    GC.Collect();
    
                    try
                    {
                        var LoopScope_cts = new CancellationTokenSource();
                        Console.WriteLine("Enter number of Sleep.For() iterations:");
                        var Loops = int.Parse(Console.ReadLine());
    
                        Console.WriteLine("Enter millisecond interval per iteration:");
                        var Rate = int.Parse(Console.ReadLine());
    
                        RunMethod = () => SomeMethod(Loops, Rate, MainScope_cts.Token);
    
                        RunMethod();
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                    }
                    Console.WriteLine("\r\nPress any key to try again, or press Escape to exit.");
                }
                while (Console.ReadKey().Key != ConsoleKey.Escape);
                Console.WriteLine("\r\nProgram Ended...");
            }
    
            private static void SomeMethod(int p_loops, int p_rate, CancellationToken p_token)
            {
                var local_cts = new CancellationTokenSource();
                Console.WriteLine("Method Executing " + p_loops + " Loops at " + p_rate + "ms each.\r\n");
                for (int i = 0; i < p_loops; i++)
                {
                    var Handles = Performance.HandleCount();
                    Sleep.For(p_rate, p_token); /*<--- Change token here to test GC and variable Scoping*/
                    Console.WriteLine("H_pre " + Handles + ", H_post " + Performance.HandleCount());
                }
            }
        }
    }
    

    性能(助手类)

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Runtime.InteropServices;
    using System.Management;
    using Common.Extensions;
    using System.Diagnostics;
    
    namespace Common.Environment
    {
        public static partial class Performance
        {
            //https://stackoverflow.com/a/9543180/1718702
            [DllImport("User32")]
            extern public static int GetGuiResources(IntPtr hProcess, int uiFlags);
    
            public static int GDI_Objects_Count()
            {
                //Return the count of GDI objects.
                return GetGuiResources(System.Diagnostics.Process.GetCurrentProcess().Handle, 0);
            }
            public static int USER_Objects_Count()
            {
                //Return the count of USER objects.
                return GetGuiResources(System.Diagnostics.Process.GetCurrentProcess().Handle, 1);
            }
            public static string CPU_Percent_Load()
            {
                //http://allen-conway-dotnet.blogspot.ca/2013/07/get-cpu-usage-across-all-cores-in-c.html
                //Get CPU usage values using a WMI query
                ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT * FROM Win32_PerfFormattedData_PerfOS_Processor");
                var cpuTimes = searcher.Get()
                    .Cast<ManagementObject>()
                    .Select(mo =>
                        new
                        {
                            Name = mo["Name"],
                            Usage = mo["PercentProcessorTime"]
                        }
                    ).ToList();
    
                var Total = cpuTimes[cpuTimes.Count - 1];
                cpuTimes.RemoveAt(cpuTimes.Count - 1);
    
                var PercentUsage = string.Join("_", cpuTimes.Select(x => Convert.ToInt32(x.Usage).ToString("00")));
    
                return PercentUsage + "," + Convert.ToInt32(Total.Usage).ToString("00");
            }
            public static long PrivateMemorySize64()
            {
                using (var P = Process.GetCurrentProcess())
                {
                    return P.PrivateMemorySize64;
                }
            }
            public static int ThreadCount()
            {
                using (var P = Process.GetCurrentProcess())
                {
                    return P.Threads.Count;
                }
            }
            public static int HandleCount()
            {
                using (var P = Process.GetCurrentProcess())
                {
                    return P.HandleCount;
                }
            }
        }
    }
    

    2013-10-18 更新:

    长期的结果。无需更改其他代码即可解决此问题。
    Graph of Application performance over ~20 hours

    关于c# - 如何诊断句柄泄漏源,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/19409634/

    相关文章:

    c# - Convert.FromBase64String c# 上的 FormatException

    python - 尝试跨 n 个节点并行化 python 循环写入字典

    CommandBars 的 VB.NET 等效项

    c# - 右键单击菜单,然后单击指向类的项目

    c# - 避免闭包中的转义值

    c# - 如何跳过跳过未授权文件的目录中的所有文件?

    multithreading - `isync` 是否会阻止 CPU PowerPC 上的存储加载重新排序?

    c# - 多线程http请求

    c# - 锁定一个winforms控件

    c# CSV FILE 如何删除日期已过的行