使用线程时内存泄漏

我似乎在这段代码中有内存泄漏。 它是一个控制台应用程序,它创建了几个类(WorkerThread),每个类都以指定的时间间隔写入控制台。 Threading.Timer用于执行此操作,因此写入控制台是在单独的线程中执行的(TimerCallback在从ThreadPool获取的单独线程中调用)。 更复杂的是,MainThread类挂钩到FileSystemWatcher的Changed事件; 当test.xml文件更改时,将重新创建WorkerThread类。

每次保存文件时(每次重新创建WorkerThread和Timer时),任务管理器中的内存都会增加(内存使用情况,有时还会增加虚拟机大小); 此外,在.Net Memory Profiler(v3.1)中,WorkerThread类的未分配实例增加了两个(这可能是一个红色的鲱鱼,因为我已经读过.Net Memory Profiler有一个错误,它很难检测到处理class级。

无论如何,这是代码 – 有谁知道什么是错的?

编辑 :我已经将类创建移出FileSystemWatcher.Changed事件处理程序,这意味着WorkerThread类总是在同一个线程中创建。 我为静态变量添加了一些保护。 我还提供了线程信息,以更清楚地显示正在发生的事情,并使用Timer与显式线程交换; 然而,记忆仍然在泄漏! Mem Usage一直缓慢增加(这只是由于控制台窗口中的额外文本?),并且当我更改文件时VM大小增加。 这是代码的最新版本:

编辑当你写入它时,这似乎主要是控制台使用内存的问题。 显式编写的Threads仍然存在增加内存使用量的问题。 请参阅下面的答案 。

class Program { private static List threads = new List(); 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 } 

你有两个问题,都是分开的:

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

此回调可以在多个不同的线程上重复触发(它使用系统线程池)。您的代码假定一次只有一个线程会执行此方法,因为可以创建线程但不能停止线程。

想象一下:线程A和B.

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

您现在在堆上有4个WorkerThread实例,但只有两个引用它们的变量,A创建的两个已泄露。 使用计时器进行事件处理和回调注册意味着泄漏的WorkerThreads保持活动(在GC意义上),尽管您的代码中没有引用它们。 他们永远被泄露。

设计中还有其他缺陷,但这是一个至关重要的缺陷。

不,不,不,不,不,不,不。 永远不要使用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; } } } 

好吧,有一段时间再次研究这个问题,看来内存泄漏有点像红鲱鱼。 当我停止写入控制台时,内存使用量会停止增加

但是,每次我编辑test.xml文件时都会有一个问题(它会触发FileSystemWatcher上的Changed事件,其处理程序设置的标志会导致工作程序类被更新,因此线程/计时器将被停止),内存增加大约4K,假设我使用显式线程,而不是定时器。 当我使用Timer时,没有问题。 但是,鉴于我宁愿使用Timer而不是Thread,这对我来说不再是一个问题,但我仍然会对它为什么会出现感兴趣。

请参阅下面的新代码。 我创建了两个类 – WorkerThread和WorkerTimer,其中一个使用Threads和其他Timers(我尝试了两个Timers,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; ///  /// Initializes a new instance of the  class. ///  /// The name. /// The interval, in seconds. 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 } 

那么你永远不会在WorkerThread实例上调用dispose

发生监视文件事件时,不会处理实际的工作线程。 我想我会重写这个,以便不创建新的线程,但它们是重新初始化的。 而不是调用Stop并重新创建线程,而是调用刚刚停止并重置计时器的新Restart方法。

永远不会终止线程 – 使用像Process Explorer这样的东西来检查线程数是否增加以及内存。 在Stop()方法中添加对Abort()的调用。

编辑:你做了,谢谢。