如何诊断Handle泄漏源

时间:2013-10-16 17:15:30

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

问题

我昨天刚刚进行了一些性能记录,因为我注意到很久以前在观察任务管理器时出现了句柄泄漏,尽管修复它的优先级不高。这是隔夜运行,每10秒钟一次采样。

由于时间限制,我还没有把它运行到失败状态,我的测试计算机也是我的开发计算机所以在编写代码时运行这个并不理想...所以我不确定它是否/何时会崩溃,但我非常怀疑这只是一个时间问题。

Graph of application resource usages and performance

注意:区域中的红色框是我“停止”工作循环并在短暂停顿后重新启动它的地方。线程在“停止”时从~100下降到~20。在从约62,000到约40,000之间约30秒后重新启动循环之前,手柄没有下降。所以一些句柄正在进行GC,只是没有我想象的那么多。我无法弄清楚root是什么阻止了所有这些句柄被收集或者它们最初来自哪里(即任务,GUI,文件等)。

如果您已经知道可能导致此问题的原因,则无需再进一步阅读。我已经提供了其余的这些信息和代码,以便以枪击式的方式来解决这个问题。我会删除,编辑等因为根本原因缩小了。出于同样的原因,如果感兴趣的东西丢失了,请告诉我,我会尝试提供它(日志,转储等)。


我做了什么

就我自己而言,我已经在Tracking Handle Misuse上完成了这个教程,并且在查看转储文件时找到了Handles打开和关闭的位置......但是它的数量太多了任何意义,我无法加载符号,所以指针只是胡言乱语。

我还没有在我的清单上查看以下两个,但是想知道是否有一些更友好的方法......

我还将我怀疑可能导致此问题的代码拆分为另一个小应用程序,并且所有内容似乎都没有问题地收集垃圾(虽然执行模式与真实应用程序相比大大简化)。

潜在罪魁祸首

我确实有几个长期存在的实例类,只要应用程序打开就会持续,包括每个只创建一次的5个表单,然后根据需要隐藏/显示。我使用主对象作为我的应用程序控制器,然后通过事件将模型和视图连接到Presenter,以Presenter-First模式。

以下是我在此应用程序中所做的一些事情,可能重要也可能不重要:

  • 广泛使用自定义ActionFunc和lambdas,其中一些可能是长寿的
  • 3个事件的自定义委托,可以为Task生成异步执行。
  • 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()'扩展只是将委托包装在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;
        }
    }
}

1 个答案:

答案 0 :(得分:1)

到目前为止,所有评论都非常有用,我发现至少有一个我的句柄泄漏源是Sleep.For()方法。我仍然认为我已经处理了漏洞,但速度明显变慢,我现在也明白了 为什么 它们正在泄漏。

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

顺便说一句,昨晚我发现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