13

問題

かなり前にタスク マネージャーを監視していてハンドル リークが発生していることに気付いたので、昨日パフォーマンス ログを記録しましたが、修正の優先度は低かったです。これは、10 秒ごとにサンプルを使用して一晩実行します。

時間の制約により、まだこれを実行して失敗したことはありません。また、テスト コンピューターは開発用コンピューターでもあるため、コードを記述しながらこれを実行するのは理想的ではありません。クラッシュするかどうか、いつクラッシュするかはわかりませんが、時間の問題であると強く疑われます。

アプリケーション リソースの使用状況とパフォーマンスのグラフ

注:領域内の赤いボックスは、作業ループを「停止」し、短い一時停止の後に再開した場所です。「停止」でスレッド数が ~100 から ~20 に減少しました。約 30 秒後にループが再開されるまで、ハンドルはドロップしませんでした。そのため、いくつかのハンドルが GC されていますが、期待するほど多くはありません。これらすべてのハンドルが収集されるのを妨げているルートや、それらが最初にどこから来たのか (つまり、タスク、GUI、ファイルなど) がわかりません。

この問題の原因がすでにわかっている場合は、これ以上読む必要はありません。この情報とコードの残りの部分は、問題を解決する散弾銃スタイルのアプローチで参照できるように提供しました。根本原因が絞り込まれ次第、削除、編集等を行います。同様に、興味のあるものが欠けている場合はお知らせください。それを提供しようとします (ログ、ダンプなど)。


私がしたこと

私自身、ハンドルの誤用の追跡に関するこのチュートリアルを実行し、ダンプ ファイルを調べて、ハンドルが開いたり閉じたりする場所を見つけました。そして、シンボルをロードするのに苦労したので、ポインターは私には意味不明でした.

リストにある次の2つはまだ行っていませんが、最初にもっと友好的な方法があるかどうか疑問に思いました...

また、これの潜在的な原因であると思われるコードを別の小さなアプリケーションに分割したところ、すべてが問題なくガベージ コレクションされたように見えました (ただし、実行パターンは実際のアプリに比べて大幅に単純化されていました)。

潜在的な犯人

アプリケーションが開いている限り存続するインスタンス化された長寿命のクラスがいくつかあります。これには、それぞれ 1 回だけ作成され、必要に応じて非表示/表示される 5 つのフォームが含まれます。メイン オブジェクトをアプリケーション コントローラーとして使用し、モデルとビューをイベント経由でプレゼンター ファースト パターンのプレゼンターに接続します。

以下は、私がこのアプリケーションで行ういくつかのことですが、重要である場合とそうでない場合があります。

  • custom と lambda を広範囲に使用ActionFuncてください。
  • イベント用の 3 つのカスタム デリゲートTask。非同期実行用に s を生成できます。
  • を安全に呼び出すための拡張機能Controls
  • Taskand Parallel.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;
        }
    }
}
4

1 に答える 1

4

これまでのすべてのコメントは非常に役に立ち、少なくとも 1 つのハンドル リークの原因がそのSleep.For()方法であることがわかりました。私はまだハンドルが漏れていると思いますが、速度が大幅に遅くなり、なぜ漏れていたのかがよくわかりました。

これは、渡されたトークンのスコープと、using ステートメントのメソッド内のローカル トークンのクリーンアップに関係していました。Eventこれを修正すると、Process 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 に答える