2009-01-27 95 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; 
     } 
    }  
}