2009-01-27 102 views
2

我在這段代碼中似乎有內存泄漏。它是一個控制檯應用程序,它創建了幾個類(WorkerThread),每個類都以指定的時間間隔寫入控制檯。 Threading.Timer用於執行此操作,因此寫入控制檯是在單獨的線程中執行的(TimerCallback在從ThreadPool獲取的單獨線程中調用)。複雜的是,MainThread類掛接到FileSystemWatcher的Changed事件;當test.xml文件更改時,將重新創建WorkerThread類。每次保存文件時(每次重新創建WorkerThread並因此重新創建Timer),任務管理器中的內存將增加(內存使用率,有時也包括虛擬機大小)。此外,在.Net內存分析器(v3.1)中,WorkerThread類的未配置實例增加了兩個(但這可能是一個紅鯡魚,因爲我已經讀過.Net內存分析器有一個錯誤,它試圖檢測。處置類使用線程時內存泄漏

總之,這裏的代碼 - 沒有人知道什麼是錯

編輯:我搬到類創作出FileSystemWatcher.Changed事件處理程序,這意味着的WorkerThread類始終?我已經爲靜態變量添加了一些保護,我還提供了線程信息以更清楚地顯示發生了什麼,並且使用Timer使用顯式線程交換;但是,t他的記憶仍在泄漏!內存使用量一直在緩慢增加(這是否僅僅是由於控制檯窗口中的額外文本?),並且當我更改文件時,VM大小會增加。以下是最新版本的代碼:

編輯這似乎主要是控制檯在寫入內存時使用內存的問題。明確寫入的線程仍然存在增加內存使用率的問題。見my answer below

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 
} 
+0

你可以做沒有計時器的工作,看看它是否仍然顯得漏?它可以幫助您確定計時器是否是實際問題。 – StingyJack 2009-01-27 13:05:13

+0

好主意。用一個明確的線程替換定時器,該線程只需等待產生完全相同的結果。 – darasd 2009-01-27 14:51:37

+0

一時興起,我將(新)代碼複製到一個新項目中並嘗試使用它 - 它完全符合標準垃圾回收行爲以「泄漏」它的方式。在主循環中強制GC.Collect()完全停止泄漏(顯然,性能成本)。我錯過了什麼嗎? – lotsoffreetime 2009-01-29 15:43:25

回答

2

嗯,有過一些時間來研究這個再次,它似乎是內存泄漏是有點紅鯡魚。 當我停止寫入控制檯時,內存使用停止增加

但是,還有一個問題是我每次編輯test.xml文件(在FileSystemWatcher上觸發Changed事件,其處理程序設置標誌導致工作程序類更新,因此線程/定時器被停止),內存增加大約4K,只要我使用明確的線程,而不是定時器。當我使用計時器時,沒有問題。但是,由於我寧願使用定時器而不是線程,這對我來說不再是問題,但我仍然會對它發生的原因感興趣。

查看下面的新代碼。我創建了兩個類 - WorkerThread和WorkerTimer,其中一個使用線程和其他計時器(我已經嘗試了兩個計時器,System.Threading.Timer和System.Timers.Timer,並打開了控制檯輸出,您可以看出它與引發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 
} 
0

那麼你永遠不會在WorkerThread實例上調用dispose

+0

但是,我確實呼叫停止,其代碼完全相同。 – darasd 2009-01-27 13:45:47

+0

您是否確定它與Profilers的觀點相同? – 2009-01-27 14:33:06

0

當發生監視的文件事件時,實際工作線程不會被放置。我想我會重寫這個,以便不會創建新線程,但它們會被重新初始化。不要調用Stop並重新創建線程,而是調用剛剛停止並重置計時器的新方法Restart

+0

然而,這是一種可能性,但是,test.xml文件將確定要創建多少個WorkerThread類,所以最終我將不得不創建並銷燬WorkerThread實例,而不是重用它們。 – darasd 2009-01-27 13:56:45

0

您從不終止線程 - 使用像Process Explorer這樣的東西來檢查線程數是否增加以及內存。在Stop()方法中添加對Abort()的調用。

編輯:你沒有,謝謝。

8

你有兩個問題,這兩個是分開的:

在Watcher.Changed的處理程序中調用了Thread.Sleep(3000); 這是在你不擁有的線程的回調中不好的行爲(因爲它是由觀察者擁有/使用的池提供的,但這不是你問題的根源。這也直接違反了guidelines for use

您可以使用靜態遍佈這是可怕的,並可能導致你到這個問題的地方:

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. 甲thread1.Stop()
  2. 甲thread2.Stop()
  3. 乙thread1.Stop()
  4. 乙thread2.Stop()
  5. 甲線程1 =新的WorkerThread()
  6. 甲線程2 =新的WorkerThread()
  7. 乙線程1 =新的WorkerThread()
  8. 乙線程2 =新的WorkerThread()

你現在有引用他們在堆4個的WorkerThread實例,但只有兩個變量,由A創建的兩個已經泄漏。使用計時器進行事件處理和回調註冊意味着儘管您在代碼中沒有提及它們,但泄漏的WorkerThreads仍保持活動狀態(從GC的角度來看)。他們永遠不會泄露。

設計還存在其他缺陷,但這是一個關鍵問題。

3

不,不,不,不,不,不。永遠不要使用Thread.Abort()。

閱讀MSDN docs就可以了。


該線程不保證立即中止或完全中止。如果線程在作爲中止過程的一部分調用的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; 
     } 
    }  
}