2013-10-16 19 views
7

Il problemaCome diagnosticare fonte di perdita di handle

ho appena messo in qualche registrazione delle prestazioni di ieri, come ho notato un problema di handle di guardare Task Manager po 'di tempo fa, anche se il fissaggio è stato a bassa priorità. Questa è una corsa notturna con un campione ogni 10 secondi.

Non ho ancora eseguito questo al guasto, a causa di vincoli di tempo e il mio computer di test è anche il mio computer di sviluppo in modo da eseguirlo mentre scrivere codice non è l'ideale ... quindi non sono sicuro se/quando lo farà incidente, ma ho il sospetto che sia solo questione di tempo.

Graph of application resource usages and performance

Nota: Il rosso inscatolato nella regione è dove "fermato" ciclo di lavoro e riavviato dopo un breve pausa. I thread sono caduti su "stop" da ~ 100 a ~ 20. The Handles non è caduto finché il loop non è stato riavviato dopo circa 30 secondi da ~ 62.000 a ~ 40.000. Quindi alcune maniglie stanno ricevendo GC, solo non quasi il numero che mi aspetto dovrebbe essere. Non riesco a capire quale sia la radice che impedisce a tutti questi handle di essere raccolti o da dove provengono (ad esempio, attività, GUI, file ecc.).

Se hai già un'idea di cosa potrebbe causare questo problema, non è necessario leggere più oltre. Ho fornito il resto di queste informazioni e il codice di riferimento in un approccio stile colpo di pistola per scoprire il problema. Rimuoverò, modificherò, ecc. Poiché la causa principale è ridotta. Per lo stesso motivo, se manca qualcosa di interessante fammelo sapere e cercherò di fornirlo (log, discariche, ecc.).


quello che ho fatto

Sul mio Ho passato attraverso questo tutorial su Tracking Handle Misuse e ottenuto per quanto riguarda guardare i file di dump per trovare dove le maniglie Apri e Chiudi ... comunque era troppo opprimente con migliaia di maniglie per avere un senso e ho avuto problemi a caricare i simboli in modo che i puntatori fossero semplicemente incomprensibili per me.

Devo ancora passare attraverso i seguenti due sulla mia lista, ma si chiedeva se ci sono stati alcuni metodi amichevoli prima ...

Ho anche Suddiviso il codice che sospettavo essere le potenziali cause di ciò in un'altra piccola applicazione e tutto appariva per ottenere Garbage Collected senza problemi (anche se il modello di esecuzione era notevolmente semplificato rispetto alla vera app).

potenziali colpevoli

Io ho diverse classi istanziate lunga durata che durano fino a quando l'applicazione è aperta per, tra cui 5 moduli creati solo una volta e poi mostrate/nascoste, se necessario. Io uso un oggetto principale come controller dell'applicazione e quindi Modelli e viste sono cablati tramite eventi ai Presentatori in uno schema di Presenter-First.

Qui di seguito sono alcune cose che faccio in questa applicazione, che può o non può essere importante:

  • Utilizzare personalizzati Action, Func e lambda estesamente, alcuni dei quali possono essere longevo
  • 3 personalizzato delegati per eventi e che possono generare spa Task s per l'esecuzione asincrona.
  • Estensione per invocazione sicura su Controls.
  • Molto, molto pesantemente utilizzare Task e Parallel.For/Parallel.Foreach per eseguire i metodi di lavoro (o eventi come detto sopra)
  • Non usare mai Thread.Sleep(), ma invece un costume Sleep.For(), che utilizza un AutoResetEvent.

principale Loop

Il flusso generale di questa applicazione quando è esecuzione si basa su un loop su una serie di file nella Offline versione e l'interrogazione di un segnale di ingresso digitale nella versione online. Di seguito è riportato il codice sudo con i commenti per la versione Offline che è ciò che posso eseguire dal mio computer portatile senza la necessità di hardware esterno e quello che il grafico sopra stava monitorando (non ho accesso all'hardware per Online modalità in questo momento).

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

Async Eventi

Di seguito è riportato l'estensione per gli eventi e la maggior parte vengono eseguiti utilizzando l'opzione di default asincrono. Le estensioni 'TryRaising()' racchiudono i delegati in un try-catch e registrano eventuali eccezioni (mentre non rilanciano non fanno parte del normale flusso di programma perché siano responsabili della cattura delle eccezioni).

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 Safe-Invoke

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

Tutto quello che posso dire è "buona fortuna" ... Questo è difficile, e di solito è molto difficile per un terzo (volontario) fare molti progressi in qualcosa di così complesso. Ma i miei compagni SO non cessano mai di stupirmi. – Floris

+4

Scarica Process Explorer e seleziona la tua app. Quindi selezionare sotto Visualizza -> riquadro inferiore - maniglie. Questo dovrebbe darti un'idea del tipo di handle che stai perdendo (Mutex, Event, File, ...) Se si tratta di un mutex o di un handle di file, dovresti avere una buona possibilità di trovare direttamente da dove provengono i problemi. –

+0

@AloisKraus Sì, ho già PE, ma non so come usarlo in tutta la sua estensione. Vedo forse 200 maniglie nell'elenco, sebbene la mia app (appena riavviata) stia usando circa ~ 2500 in questo momento. ~ 30 'File' e ~ 40' Key' che sembrano entrambi stabili, ma molte maniglie 'Thread' che vengono create e distrutte (le evidenziazioni rosso/verde vengono aggiunte e rimosse). Solo 4 tipi 'Event' e 6' Mutant' nell'elenco. Per lo più fuoco e dimentico le attività, non le dispongo o di solito aspetto. Rilevo le eccezioni all'interno di Google Task, le registro e poi le restituisco ... lasciando che il compito giri su se stesso. – HodlDwon

risposta

1

Tutti i commenti che sono stati finora piuttosto disponibile e ho trovato almeno un la fonte della mia perdita di handle è il metodo Sleep.For(). Continuo a pensare di avere perdite di handle, ma a un ritmo significativamente più lento e ho anche capito meglio ora perché stavano perdendo.

Ha avuto a che fare con l'ambito del token passato e la pulizia del token locale all'interno del metodo in un'istruzione using. Una volta risolto questo problema, ho iniziato a visualizzare tutti gli handle senza nome Event in Process Explorer che sono stati creati e distrutti invece di stare semplicemente seduti lì.

Per inciso, ho trovato Anatomy of a "Memory Leak" ieri sera tardi e imparerò di più su Windbg per ulteriori indagini.

Sto anche eseguendo un test delle prestazioni di lunga durata per verificare se questa fosse l'unica perdita o meno e rivedere altre sezioni del mio codice che utilizzano WaitHandles per accertarmi di averle correttamente portata e smaltirle.

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

Test App (Console)

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

Performance (classe di supporto)

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

Aggiornamento 2013-10-18:

Risultati del lungo periodo. Non sono state necessarie altre modifiche al codice per risolvere questo problema.