如何诊断Handle泄漏源

Hod*_*won 8 c# multithreading memory-leaks winforms c#-4.0

问题

我昨天刚刚进行了一些性能记录,因为我注意到很久以前看过任务管理器的句柄泄漏,虽然修复它的优先级不高.这是隔夜运行,每隔10秒进行一次采样.

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

应用资源使用和性能图

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

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


我做了什么

就我自己而言,我已经完成了关于跟踪处理滥用的教程,并且在查看转储文件时找到了Handles打开和关闭的位置...但是它只是压倒了成千上万的句柄才能让人感觉到而且我无法加载Symbols,所以指针对我来说只是胡言乱语.

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

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

潜在的罪魁祸首

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

以下是我在此应用程序中执行的一些操作,可能重要也可能不重要:

  • 使用自定义Action,Func和lambda表达式广泛,其中一些可能是长寿命
  • 3个事件的自定义委托,可以Task为异步执行生成s.
  • 用于安全调用的扩展Controls.
  • 非常非常大量地使用TaskParallel.For/ Parallel.Foreach或运行工人方法(或上面提到的事件)
  • 永远不要使用Thread.Sleep(),而是使用AutoResetEvent的自定义Sleep.For().

主循环

这个应用程序时,它的一般流程运行是基于遍历的一系列文件的离线版本,并在数字输入信号的轮询在线版本.下面是带有离线版本注释的sudo-code,这是我可以从我的笔记本电脑上运行而无需外部硬件以及上面的图表正在监控的内容(我目前无法访问在线模式的硬件)).

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);
}
Run Code Online (Sandbox Code Playgroud)

异步事件

下面是事件的扩展,大多数是使用默认的异步选项执行的.'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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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();
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Hod*_*won 4

到目前为止,所有评论都非常有帮助,我发现我的句柄泄漏的至少一个来源就是该Sleep.For()方法。我仍然认为我有手柄泄漏,但速度明显较慢,而且我现在也更好地理解了它们泄漏的原因。

它与传入令牌的范围以及清理 using 语句中方法内的本地令牌有关。修复此问题后,我开始看到EventProcess Explorer 中所有未命名的句柄被创建和销毁,而不是仅仅坐在那里。

顺便说一句,我昨晚深夜发现了“内存泄漏”剖析,并且肯定会了解更多有关 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;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

测试应用程序(控制台)

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());
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

性能(助手类)

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;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

2013年10月18日更新:

长期来看的结果。无需更改其他代码即可解决此问题。 约 20 小时内的应用程序性能图表