C# 使用线程时内存泄漏

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/483295/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-04 05:11:49  来源:igfitidea点击:

Memory leak while using Threads

c#.netmultithreadingmemorytimer

提问by darasd

I appear to have a memory leak in this piece of code. It is a console app, which creates a couple of classes (WorkerThread), each of which writes to the console at specified intervals. The Threading.Timer is used to do this, hence writing to the console is performed in a separate thread (the TimerCallback is called in a seperate thread taken from the ThreadPool). To complicate matters, the MainThread class hooks on to the Changed event of the FileSystemWatcher; when the test.xml file changes, the WorkerThread classes are recreated.

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

Each time the file is saved, (each time that the WorkerThread and therefore the Timer is recreated), the memory in the Task Manager increases (Mem Usage, and sometimes also VM Size); furthermore, in .Net Memory Profiler (v3.1), the Undisposed Instances of the WorkerThread class increases by two (this may be a red herring though, because I've read that .Net Memory Profiler had a bug whereby it struggled to detect disposed classes.

每次保存文件(每次重新创建 WorkerThread 和 Timer 时),任务管理器中的内存都会增加(Mem Usage,有时还有 VM Size);此外,在 .Net Memory Profiler (v3.1) 中,WorkerThread 类的 Undisposed Instances 增加了两个(这可能是一个红鲱鱼,因为我读过 .Net Memory Profiler 有一个错误,它很难检测到处置类。

Anyway, here's the code - does anyone know what's wrong?

无论如何,这是代码 - 有谁知道出了什么问题?

EDIT: I've moved the class creation out of the FileSystemWatcher.Changed event handler, meaning that the WorkerThread classes are always being created in the same thread. I've added some protection to the static variables. I've also provided threading information to show more clearly what's going on, and have been interchanging using the Timer with using an explicit Thread; however, the memory is still leaking! The Mem Usage increases slowly all the time (is this simply due to extra text in the console window?), and the VM Size increases when I change the file. Here is the latest version of the code:

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

EDITThis appears to be primarily a problem with the console using up memory, as you write to it. There is still a problem with explicitly written Threads increasing the memory usage. See 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
}

采纳答案by darasd

Well, having had some time to look into this again, it appears that the memory leak is a bit of a red herring. When I stop writing to the console, the memory usage stops increasing.

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

However, there is a remaining issue in that every time I edit the test.xml file (which fires the Changed event on the FileSystemWatcher, whose handler sets flags that cause the worker classes to be renewed and therefore threads/timers to be stopped), the memory increases by about 4K, providing that I am using explicit Threads, rather Timers. When I use a Timer, there is no problem. But, given that I would rather use a Timer than a Thread, this is no longer an issue to me, but I would still be interested in why it is occuring.

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

See the new code below. I've created two classes - WorkerThread and WorkerTimer, one of which uses Threads and the other Timers (I've tried two Timers, the System.Threading.Timer and the System.Timers.Timer. with the Console output switched on, you can see the difference that this makes with regards to which thread the tick event is raised on). Just comment/uncomment the appropriate lines of MainThread.Start in order to use the required class. For the reason above, it is recommended that the Console.WriteLine lines are commented out, except when you want to check that everything is working as expected.

请参阅下面的新代码。我创建了两个类 - 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
}

回答by Greg Dean

Well you never actually call disposeon the WorkerThread instances.

好吧,您实际上从未调用dispose过 WorkerThread 实例。

回答by tvanfosson

The actual worker threads aren't being disposed when the watched file event occurs. I think I would rewrite this so that new threads aren't created, but they are reinitialized. Instead of calling Stopand recreating the threads, call a new Restartmethod that just stops and resets the timer.

当被监视的文件事件发生时,实际的工作线程并没有被释放。我想我会重写这个,这样就不会创建新线程,而是重新初始化它们。不是调用Stop和重新创建线程,而是调用一个新Restart方法来停止和重置计时器。

回答by lotsoffreetime

You never terminate the threads - use something like Process Explorer to check whether the thread count is increasing as well as memory. Add a call to Abort() in your Stop() method.

您永远不会终止线程 - 使用诸如 Process Explorer 之类的工具来检查线程数和内存是否在增加。在 Stop() 方法中添加对 Abort() 的调用。

Edit: You did, thanks.

编辑:你做到了,谢谢。

回答by ShuggyCoUk

You have two issues, both separate:

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

In Watcher.Changed's handler you call Thread.Sleep(3000); This is poor behaviour in a callback of a thread you do not own (since it is being supplied by the pool owned/used by the watcher. This is not the source of your problem though. This it in direct violation of the guidelines for use

在 Watcher.Changed 的​​处理程序中,您调用 Thread.Sleep(3000); 这是您不拥有的线程的回调中的不良行为(因为它是由观察者拥有/使用的池提供的。但这不是问题的根源。这直接违反了使用指南

You use statics all over the place which is horrible, and has likely led you into this problem:

您到处都在使用静力学,这很可怕,并且可能使您陷入了这个问题:

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

This callback can fire repeatedly on multiple different threads (it uses the system thread pool for this) You code assumes that only one thread will ever execute this method at a time since threads can be created but not not stopped.

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

Imagine: thread A and B

想象一下:线程 A 和 B

  1. A thread1.Stop()
  2. A thread2.Stop()
  3. B thread1.Stop()
  4. B thread2.Stop()
  5. A thread1 = new WorkerThread()
  6. A thread2 = new WorkerThread()
  7. B thread1 = new WorkerThread()
  8. B thread2 = new WorkerThread()
  1. 一个线程1.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()

You now have 4 WorkerThread instances on the heap but only two variables referencing them, the two created by A have leaked. The event handling and callback registration with the timer means that theses leaked WorkerThreads are kept alive (in the GC sense) despite you having no reference to them in your code. they stay leaked for ever.

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

There are other flaws in the design but this is a critical one.

设计中还有其他缺陷,但这是一个关键缺陷。

回答by Rob

No, no, no, no, no, no, no. Never use Thread.Abort().

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

Read the MSDN docson it.

阅读有关它的MSDN 文档



The thread is not guaranteed to abort immediately, or at all. This situation can occur if a thread does an unbounded amount of computation in the finally blocks that are called as part of the abort procedure, thereby indefinitely delaying the abort. To wait until a thread has aborted, you can call the Join method on the thread after calling the Abort method, but there is no guarantee the wait will end.

该线程不能保证立即中止,或者根本不中止。如果线程在作为中止过程的一部分调用的 finally 块中进行无限量的计算,从而无限期地延迟中止,就会发生这种情况。要等到线程中止,您可以在调用 Abort 方法后对该线程调用 Join 方法,但不能保证等待会结束。



The correct way to end a thread is to signal to it that it should end, then call Join() on that thread. I usually do something like this (pseudo-code):

结束线程的正确方法是通知它应该结束,然后在该线程上调用 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;
        }
    }    
}