13

问题

我昨天刚刚输入了一些性能记录,因为我注意到很久以前观看任务管理器的句柄泄漏,尽管修复它一直是低优先级。这是隔夜运行,每 10 秒采样一次。

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

应用程序资源使用情况和性能图表

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

如果您已经知道可能导致此问题的原因,则无需进一步阅读。我已经提供了其余的信息和代码,以供参考,以霰弹枪式的方法解决问题。随着根本原因的缩小,我将删除、编辑等。同样,如果缺少感兴趣的内容,请告诉我,我会尽力提供(日志、转储等)。


我做了什么

就我自己而言,我已经完成了关于跟踪句柄滥用的本教程,并查看了转储文件以找到句柄打开和关闭的位置......但是它对于成千上万的句柄来说实在是太难以理解了而且我在加载符号时遇到了麻烦,所以指针对我来说只是胡言乱语。

我还没有通过我的列表中的以下两个,但想知道是否有一些更友好的方法首先......

我还将我怀疑可能导致此问题的代码拆分到另一个小型应用程序中,并且所有内容似乎都可以毫无问题地收集垃圾(尽管与实际应用程序相比,执行模式大大简化了)。

潜在的罪魁祸首

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

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

  • 广泛使用 customActionFunclambdas,其中一些可能是长期存在的
  • 3 个用于事件的自定义委托,它们可以生成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()' 扩展只是将委托包装在 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();
            }
        }
    }
}

睡眠.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;
        }
    }
}
4

1 回答 1

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

测试应用程序(控制台)

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 日更新:

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

于 2013-10-17T14:45:44.650 回答