2013-10-16 89 views
7

的問題如何診斷句柄泄漏源

我只是把一些性能記錄昨天,我注意到從看任務管理器前一段時間一個句柄泄漏,但修復它一直是低優先級。這是一個每10秒鐘進行一次採樣的過夜運行。

我還沒有運行該故障,由於時間的限制,我的測試計算機也是這樣運行此一邊寫代碼是不理想......所以我不知道我的dev的電腦,如果/當它會崩潰,但我高度懷疑這只是時間問題。

Graph of application resource usages and performance

注:的紅色區域盒裝是我「停止」的工作循環,並在短暫的停頓後重新啓動它。線程從〜100下降到了〜20。手柄沒有下降,直到循環在約30秒後重新開始從〜62000至〜40,000。因此,一些把手越來越GC'd,只是沒有那麼多,因爲我想到應該是。我想不出有什麼根防止越來越收集所有這些手柄或他們最初是來自何處(即任務,GUI,文件等)。

如果您已經知道可能導致此問題的原因,則無需進一步閱讀。我已經提供了這個信息和代碼的其餘部分在sussing出來的問題的鳥槍式的方法參考。我將刪除,編輯等,因爲根本原因被縮小。同樣的道理,如果感興趣的東西是缺少讓我知道,我會盡力爲它(日誌,轉儲等)。


我已經完成

在我自己的,我通過這個教程Tracking Handle Misuse走了,並得到儘可能看着轉儲文件找到,其中把手打開和關閉......但是,它的數量太多,難以理解,而且我很難讓符號加載,所以指針對我來說只是一句胡言亂語。

我還沒有經過我的名單上有以下兩個,而是想知道是否有一些友好的方法第一...

我也將我懷疑這個代碼分解成另一個小應用程序的代碼,並且所有內容似乎都可以毫無問題地收集垃圾(儘管執行模式與真實應用程序相比大大簡化了)。

潛在元兇

我的確有這個最後只要應用是開了好長壽命的實例化類,包括創建只有一次每個然後隱藏/需要5個所示表格。我使用主對象作爲我的應用程序控制器,然後模型和視圖通過事件連接到演示者優先模式中的演示者。

下面是一些事情,我在此應用中,這可能是也可能不是重要的事:

  • 使用自定義ActionFunc和lambda表達式廣泛,其中一些可能是長壽命
  • 3個自定義代表參加事件並可以產生異步執行的Task
  • 安全調用Controls的擴展。
  • 非常,非常大量使用TaskParallel.For/Parallel.Foreach運行工法(或事件如上所述)
  • 不要使用Thread.sleep()方法,而是一個自定義Sleep.For(),它使用一個的AutoResetEvent。

主循環

這個應用程序時,它運行的一般流程是基於環比在離線版本的一系列文件和數字輸入信號的輪詢在在線版本。下面是有關離線版本的註釋的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; 
     } 
    } 
} 
+0

我只能說「好運」......這是一個艱難的過程,對於一個(志願者)第三方來說,通常很難在這個複雜的事情上取得很大的進步。但是我的同胞們永遠不會驚訝於我。 – Floris

+4

下載Process Explorer並選擇您的應用程序。然後選擇View - > Lower Pane - Handles。這應該給你一個想法,你正在泄漏的處理類型(互斥,事件,文件,...)如果它是一個有名的互斥或文件句柄,你應該有一個很好的機會直接找到問題的來源。 –

+0

@AloisKraus是的,我已經有體育課,只是不知道如何充分使用它。我看到列表中可能有200個句柄,儘管我的應用程序(剛剛重新啓動)現在正在使用約2500個。 〜30'File'和〜40'Key'看起來很穩定,但是很多正在創建和銷燬的Thread線程處理(紅/綠突出顯示被添加和刪除)。列表中只有4個'Event'和6'Mutant'類型。我主要是開火併忘記任務,我不會處理它們或者通常等待。我確實在任務內部捕獲異常,記錄它們,然後返回......讓任務自行減速。 – HodlDwon

回答

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