2

このコードでメモリ リークが発生しているようです。これはコンソール アプリであり、いくつかのクラス (WorkerThread) を作成し、それぞれが指定された間隔でコンソールに書き込みます。これには Threading.Timer が使用されるため、コンソールへの書き込みは別のスレッドで実行されます (TimerCallback は ThreadPool から取得した別のスレッドで呼び出されます)。さらに複雑なことに、MainThread クラスは FileSystemWatcher の Changed イベントにフックします。test.xml ファイルが変更されると、WorkerThread クラスが再作成されます。

ファイルが保存されるたびに (WorkerThread、したがってタイマーが再作成されるたびに)、タスク マネージャーのメモリが増加します (メモリ使用量、場合によっては VM サイズも)。さらに、.Net Memory Profiler (v3.1) では、WorkerThread クラスの Undisposed Instances が 2 増加します (ただし、.Net Memory Profiler には検出に苦労するバグがあったことを読んだので、これは危険かもしれません)。破棄されたクラス。

とにかく、ここにコードがあります-誰かが何が悪いのか知っていますか?

編集: クラスの作成を FileSystemWatcher.Changed イベント ハンドラーから移動しました。つまり、WorkerThread クラスは常に同じスレッドで作成されます。静的変数にいくつかの保護を追加しました。また、何が起こっているかをより明確に示すためにスレッド情報を提供し、Timer の使用と明示的な Thread の使用を交換してきました。ただし、メモリはまだリークしています。メモリ使用量は常にゆっくりと増加し (これは単にコンソール ウィンドウの余分なテキストが原因ですか?)、ファイルを変更すると VM サイズが増加します。コードの最新バージョンは次のとおりです。

編集これは、主に、書き込み時にコンソールがメモリを使い果たすという問題のようです。明示的に記述されたスレッドがメモリ使用量を増加させるという問題はまだあります。以下の私の答えを見てください。

class Program
{
    private static List<WorkerThread> threads = new List<WorkerThread>();

    static void Main(string[] args)
    {
        MainThread.Start();

    }
}

public class MainThread
{
    private static int _eventsRaised = 0;
    private static int _eventsRespondedTo = 0;
    private static bool _reload = false;
    private static readonly object _reloadLock = new object();
    //to do something once in handler, though
    //this code would go in onStart in a windows service.
    public static void Start()
    {
        WorkerThread thread1 = null;
        WorkerThread thread2 = null;

        Console.WriteLine("Start: thread " + Thread.CurrentThread.ManagedThreadId);
        //watch config
        FileSystemWatcher watcher = new FileSystemWatcher();
        watcher.Path = "../../";
        watcher.Filter = "test.xml";
        watcher.EnableRaisingEvents = true;
        //subscribe to changed event. note that this event can be raised a number of times for each save of the file.
        watcher.Changed += (sender, args) => FileChanged(sender, args);

        thread1 = new WorkerThread("foo", 10);
        thread2 = new WorkerThread("bar", 15);

        while (true)
        {
            if (_reload)
            {
                //create our two threads.
                Console.WriteLine("Start - reload: thread " + Thread.CurrentThread.ManagedThreadId);
                //wait, to enable other file changed events to pass
                Console.WriteLine("Start - waiting: thread " + Thread.CurrentThread.ManagedThreadId);
                thread1.Dispose();
                thread2.Dispose();
                Thread.Sleep(3000); //each thread lasts 0.5 seconds, so 3 seconds should be plenty to wait for the 
                                    //LoadData function to complete.
                Monitor.Enter(_reloadLock);
                thread1 = new WorkerThread("foo", 10);
                thread2 = new WorkerThread("bar", 15);
                _reload = false;
                Monitor.Exit(_reloadLock);
            }
        }
    }

    //this event handler is called in a separate thread to Start()
    static void FileChanged(object source, FileSystemEventArgs e)
    {
        Monitor.Enter(_reloadLock);
        _eventsRaised += 1;
        //if it was more than a second since the last event (ie, it's a new save), then wait for 3 seconds (to avoid 
        //multiple events for the same file save) before processing
        if (!_reload)
        {
            Console.WriteLine("FileChanged: thread " + Thread.CurrentThread.ManagedThreadId);
            _eventsRespondedTo += 1;
            Console.WriteLine("FileChanged. Handled event {0} of {1}.", _eventsRespondedTo, _eventsRaised);
            //tell main thread to restart threads
            _reload = true;
        }
        Monitor.Exit(_reloadLock);
    }
}

public class WorkerThread : IDisposable
{
    private System.Threading.Timer timer;   //the timer exists in its own separate thread pool thread.
    private string _name = string.Empty;
    private int _interval = 0;  //thread wait interval in ms.
    private Thread _thread = null;
    private ThreadStart _job = null;

    public WorkerThread(string name, int interval)
    {
        Console.WriteLine("WorkerThread: thread " + Thread.CurrentThread.ManagedThreadId);
        _name = name;
        _interval = interval * 1000;
        _job = new ThreadStart(LoadData);
        _thread = new Thread(_job);
        _thread.Start();
        //timer = new Timer(Tick, null, 1000, interval * 1000);
    }

    //this delegate instance does NOT run in the same thread as the thread that created the timer. It runs in its own
    //thread, taken from the ThreadPool. Hence, no need to create a new thread for the LoadData method.
    private void Tick(object state)
    {
        //LoadData();
    }

    //Loads the data. Called from separate thread. Lasts 0.5 seconds.
    //
    //private void LoadData(object state)
    private void LoadData()
    {
        while (true)
        {
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine(string.Format("Worker thread {0} ({2}): {1}", _name, i, Thread.CurrentThread.ManagedThreadId));
                Thread.Sleep(50);
            }
            Thread.Sleep(_interval);
        }
    }

    public void Stop()
    {
        Console.WriteLine("Stop: thread " + Thread.CurrentThread.ManagedThreadId);
        //timer.Dispose();
        _thread.Abort();
    }


    #region IDisposable Members

    public void Dispose()
    {
        Console.WriteLine("Dispose: thread " + Thread.CurrentThread.ManagedThreadId);
        //timer.Dispose();
        _thread.Abort();
    }

    #endregion
}
4

6 に答える 6

8

2つの問題があり、どちらも別々です。

Watcher.Changedのハンドラーで、Thread.Sleep(3000);を呼び出します。これは、所有していないスレッドのコールバックでは不適切な動作です(ウォッチャーが所有/使用しているプールによって提供されているためです。ただし、これは問題の原因ではありません。これは、使用上のガイドラインに直接違反しています。

あなたは恐ろしい場所全体で静力学を使用しており、おそらくこの問題にあなたを導きました:

static void test()
{
    _eventsRaised += 1;
    //if it was more than a second since the last event (ie, it's a new save), then wait for 3 seconds (to avoid 
    //multiple events for the same file save) before processing
    if (DateTime.Now.Ticks - _lastEventTicks > 1000)
    {
        Thread.Sleep(3000);
        _lastEventTicks = DateTime.Now.Ticks;
        _eventsRespondedTo += 1;
        Console.WriteLine("File changed. Handled event {0} of {1}.", _eventsRespondedTo, _eventsRaised);
        //stop threads and then restart them
        thread1.Stop();
        thread2.Stop();
        thread1 = new WorkerThread("foo", 20);
        thread2 = new WorkerThread("bar", 30);
    }
}

このコールバックは、複数の異なるスレッドで繰り返し発生する可能性があります(これにはシステムスレッドプールを使用します)。スレッドは作成できますが停止できないため、コードは一度に1つのスレッドのみがこのメソッドを実行することを前提としています。

想像してみてください:スレッドAとB

  1. thread1.Stop()
  2. thread2.Stop()
  3. B thread1.Stop()
  4. B thread2.Stop()
  5. thread1 = new WorkerThread()
  6. thread2 = new WorkerThread()
  7. B thread1 = new WorkerThread()
  8. B thread2 = new WorkerThread()

これで、ヒープ上に4つのWorkerThreadインスタンスがありますが、それらを参照する変数は2つだけで、Aによって作成された2つがリークしています。タイマーによるイベント処理とコールバック登録は、コード内でそれらへの参照がない場合でも、これらのリークされたWorkerThreadsが(GCの意味で)存続することを意味します。彼らは永遠に漏れたままです。

設計には他にも欠陥がありますが、これは重大な欠陥です。

于 2009-01-27T14:34:19.153 に答える
3

いや、いや、いや、いや、いや、いや、いや。Thread.Abort() は使用しないでください。

その上のMSDNドキュメントを読んでください。


スレッドがすぐに中止されるか、まったく中止されるかは保証されません。この状況は、アボート プロシージャの一部として呼び出される finally ブロックでスレッドが無限の量の計算を実行し、それによってアボートが無期限に遅延する場合に発生する可能性があります。スレッドが中止されるまで待機するには、Abort メソッドを呼び出した後にスレッドで Join メソッドを呼び出すことができますが、待機が終了する保証はありません。


スレッドを終了する正しい方法は、終了する必要があることをスレッドに通知してから、そのスレッドで Join() を呼び出すことです。私は通常、次のようなことを行います(疑似コード):

public class ThreadUsingClass
{
    private object mSyncObject = new object();
    private bool mKilledThread = false;
    private Thread mThread = null;

    void Start()
    {
        // start mThread
    }

    void Stop()
    {
        lock(mSyncObject)
        {
            mKilledThread = true;
        }

        mThread.Join();
    }

    void ThreadProc()
    {
        while(true)
        {
            bool isKilled = false;
            lock(mSyncObject)
            {
                isKilled = mKilledThread;
            }
            if (isKilled)
                return;
        }
    }    
}
于 2009-01-29T16:02:47.613 に答える
2

さて、これをもう一度調べてみたところ、メモリ リークはちょっとしたニシンのようです。コンソールへの書き込みを停止すると、メモリ使用量の増加が止まります。

ただし、test.xml ファイルを編集するたびに (これにより FileSystemWatcher で Changed イベントが発生し、そのハンドラーがワーカー クラスを更新してスレッド/タイマーを停止させるフラグを設定する)、という問題が残ります。タイマーではなく明示的なスレッドを使用している場合、メモリは約 4K 増加します。タイマーを使用する場合は問題ありません。しかし、スレッドよりもタイマーを使用したいので、これはもはや問題ではありませんが、なぜそれが発生しているのかについてはまだ興味があります.

以下の新しいコードを参照してください。WorkerThread と WorkerTimer の 2 つのクラスを作成しました。そのうちの 1 つはスレッドを使用し、もう 1 つはタイマーを使用します (System.Threading.Timer と System.Timers.Timer の 2 つのタイマーを試しました。コンソール出力をオンにすると、 tickイベントが発生するスレッドに関して、これがもたらす違いを見ることができます)。必要なクラスを使用するには、MainThread.Start の適切な行をコメント化/コメント解除するだけです。上記の理由から、すべてが期待どおりに機能していることを確認する場合を除き、Console.WriteLine の行をコメント アウトすることをお勧めします。

class Program
{
    static void Main(string[] args)
    {
        MainThread.Start();

    }
}

public class MainThread
{
    private static int _eventsRaised = 0;
    private static int _eventsRespondedTo = 0;
    private static bool _reload = false;
    private static readonly object _reloadLock = new object();
    //to do something once in handler, though
    //this code would go in onStart in a windows service.
    public static void Start()
    {
        WorkerThread thread1 = null;
        WorkerThread thread2 = null;
        //WorkerTimer thread1 = null;
        //WorkerTimer thread2 = null;

        //Console.WriteLine("Start: thread " + Thread.CurrentThread.ManagedThreadId);
        //watch config
        FileSystemWatcher watcher = new FileSystemWatcher();
        watcher.Path = "../../";
        watcher.Filter = "test.xml";
        watcher.EnableRaisingEvents = true;
        //subscribe to changed event. note that this event can be raised a number of times for each save of the file.
        watcher.Changed += (sender, args) => FileChanged(sender, args);

        thread1 = new WorkerThread("foo", 10);
        thread2 = new WorkerThread("bar", 15);
        //thread1 = new WorkerTimer("foo", 10);
        //thread2 = new WorkerTimer("bar", 15);

        while (true)
        {
            if (_reload)
            {
                //create our two threads.
                //Console.WriteLine("Start - reload: thread " + Thread.CurrentThread.ManagedThreadId);
                //wait, to enable other file changed events to pass
                //Console.WriteLine("Start - waiting: thread " + Thread.CurrentThread.ManagedThreadId);
                thread1.Dispose();
                thread2.Dispose();
                Thread.Sleep(3000); //each thread lasts 0.5 seconds, so 3 seconds should be plenty to wait for the 
                //LoadData function to complete.
                Monitor.Enter(_reloadLock);
                //GC.Collect();
                thread1 = new WorkerThread("foo", 5);
                thread2 = new WorkerThread("bar", 7);
                //thread1 = new WorkerTimer("foo", 5);
                //thread2 = new WorkerTimer("bar", 7);
                _reload = false;
                Monitor.Exit(_reloadLock);
            }
        }
    }

    //this event handler is called in a separate thread to Start()
    static void FileChanged(object source, FileSystemEventArgs e)
    {
        Monitor.Enter(_reloadLock);
        _eventsRaised += 1;
        //if it was more than a second since the last event (ie, it's a new save), then wait for 3 seconds (to avoid 
        //multiple events for the same file save) before processing
        if (!_reload)
        {
            //Console.WriteLine("FileChanged: thread " + Thread.CurrentThread.ManagedThreadId);
            _eventsRespondedTo += 1;
            //Console.WriteLine("FileChanged. Handled event {0} of {1}.", _eventsRespondedTo, _eventsRaised);
            //tell main thread to restart threads
            _reload = true;
        }
        Monitor.Exit(_reloadLock);
    }
}

public class WorkerTimer : IDisposable
{
    private System.Threading.Timer _timer;   //the timer exists in its own separate thread pool thread.
    //private System.Timers.Timer _timer;
    private string _name = string.Empty;

    /// <summary>
    /// Initializes a new instance of the <see cref="WorkerThread"/> class.
    /// </summary>
    /// <param name="name">The name.</param>
    /// <param name="interval">The interval, in seconds.</param>
    public WorkerTimer(string name, int interval)
    {
        _name = name;
        //Console.WriteLine("WorkerThread constructor: Called from thread " + Thread.CurrentThread.ManagedThreadId);
        //_timer = new System.Timers.Timer(interval * 1000);
        //_timer.Elapsed += (sender, args) => LoadData();
        //_timer.Start();
        _timer = new Timer(Tick, null, 1000, interval * 1000);
    }

    //this delegate instance does NOT run in the same thread as the thread that created the timer. It runs in its own
    //thread, taken from the ThreadPool. Hence, no need to create a new thread for the LoadData method.
    private void Tick(object state)
    {
        LoadData();
    }

    //Loads the data. Called from separate thread. Lasts 0.5 seconds.
    //
    private void LoadData()
    {
        for (int i = 0; i < 10; i++)
        {
            //Console.WriteLine(string.Format("Worker thread {0} ({2}): {1}", _name, i, Thread.CurrentThread.ManagedThreadId));
            Thread.Sleep(50);
        }
    }

    public void Stop()
    {
        //Console.WriteLine("Stop: called from thread " + Thread.CurrentThread.ManagedThreadId);
        //_timer.Stop();
        _timer.Change(Timeout.Infinite, Timeout.Infinite);
        //_timer = null;
        //_timer.Dispose();
    }


    #region IDisposable Members

    public void Dispose()
    {
        //Console.WriteLine("Dispose: called from thread " + Thread.CurrentThread.ManagedThreadId);
        //_timer.Stop();
        _timer.Change(Timeout.Infinite, Timeout.Infinite);
        //_timer = null;
        //_timer.Dispose();
    }

    #endregion
}

public class WorkerThread : IDisposable
{
    private string _name = string.Empty;
    private int _interval = 0;  //thread wait interval in ms.
    private Thread _thread = null;
    private ThreadStart _job = null;
    private object _syncObject = new object();
    private bool _killThread = false;

    public WorkerThread(string name, int interval)
    {
        _name = name;
        _interval = interval * 1000;
        _job = new ThreadStart(LoadData);
        _thread = new Thread(_job);
        //Console.WriteLine("WorkerThread constructor: thread " + _thread.ManagedThreadId + " created. Called from thread " + Thread.CurrentThread.ManagedThreadId);
        _thread.Start();
    }

    //Loads the data. Called from separate thread. Lasts 0.5 seconds.
    //
    //private void LoadData(object state)
    private void LoadData()
    {
        while (true)
        {
            //check to see if thread it to be stopped.
            bool isKilled = false;

            lock (_syncObject)
            {
                isKilled = _killThread;
            }

            if (isKilled)
                return;

            for (int i = 0; i < 10; i++)
            {
                //Console.WriteLine(string.Format("Worker thread {0} ({2}): {1}", _name, i, Thread.CurrentThread.ManagedThreadId));
                Thread.Sleep(50);
            }
            Thread.Sleep(_interval);
        }
    }

    public void Stop()
    {
        //Console.WriteLine("Stop: thread " + _thread.ManagedThreadId + " called from thread " + Thread.CurrentThread.ManagedThreadId);
        //_thread.Abort();
        lock (_syncObject)
        {
            _killThread = true;
        }
        _thread.Join();
    }


    #region IDisposable Members

    public void Dispose()
    {
        //Console.WriteLine("Dispose: thread " + _thread.ManagedThreadId + " called from thread " + Thread.CurrentThread.ManagedThreadId);
        //_thread.Abort();
        lock (_syncObject)
        {
            _killThread = true;
        }
        _thread.Join();
    }

    #endregion
}
于 2009-02-16T11:45:12.077 に答える
0

実際にdisposeWorkerThread インスタンスを呼び出すことはありません。

于 2009-01-27T13:05:23.000 に答える
0

監視対象のファイル イベントが発生したときに、実際のワーカー スレッドは破棄されません。新しいスレッドが作成されないようにこれを書き直すと思いますが、それらは再初期化されます。Stopスレッドを呼び出して再作成する代わりにRestart、タイマーを停止してリセットするだけの新しいメソッドを呼び出します。

于 2009-01-27T13:07:28.673 に答える
0

スレッドを終了することはありません。Process Explorer などを使用して、スレッド数とメモリが増加しているかどうかを確認します。Stop() メソッドに Abort() の呼び出しを追加します。

編集:そうでした、ありがとう。

于 2009-01-27T13:31:36.143 に答える