Memory leak while using Threads

25,721

Solution 1

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.

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.

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
}

Solution 2

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

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

  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()

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.

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

Solution 3

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

Read the MSDN docs on it.


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.


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):

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;
        }
    }    
}
Share:
25,721
darasd
Author by

darasd

Updated on September 08, 2020

Comments

  • darasd
    darasd over 3 years

    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.

    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.

    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:

    EDIT This 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
    }