Home > Enterprise >  Why doesn't the timer get garbage collected?
Why doesn't the timer get garbage collected?

Time:12-07

I've written a little experiment to better understand garbage collection. The scenario is this: A timer invokes a repeating event on a set time interval. No object holds a pointer to the timer. When GC occurs does the timer stop invoking its event?

My hypothesis was that given enough time and GC attempts, the timer would stop invoking its event.

Here is the code I wrote to test that:

using System;
using System.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTest;  


[TestClass]
public class TestGC
{

    //starts a repeating timer without holding on to a reference to the timer
    private void StartTimer(Action callback, int interval)
    {
        var timer = new Timer(t => callback());
        timer.Change(interval, interval);
        timer = null;//this probably does nothing, but lets just overwrite the pointer for good measure
    }



    [TestMethod]
    public void TestEventInvoker()
    {
        var count = new ThreadSafe<int>();
        var interval = 100;//time between timer events (ms)
        var totalTime = 50000;//time of the experiment (ms)
        var expectedCalls = totalTime / interval;//minimum number of times that the timer event is invoked if it isn't stopped
        StartTimer(()=>count.Value  ,interval);

        //gc periodically to make sure the timer gets GC'ed
        for (int i = 0; i < expectedCalls; i  )
        {
            GC.Collect();
            Thread.Sleep(interval);
        }

        //for debugging
        Console.WriteLine($"Expected {expectedCalls} calls. Actual: {count.Value}");

        //test passes if the timer stops before the time is over, and the minimum number of calls is not achieved
        // the -1 accounts for the edge case where the test completes just before the last timer call had a chance to be executed
        Assert.IsTrue(count.Value < (expectedCalls - 1));
    }


}

What I found is that the timer continues to invoke its event repeatedly, and that increasing the total time and number of GC calls has no effect on the stopping the timer. Example output:

Assert.IsTrue failed.
Expected 500 calls. Actual: 546

So my question is this:
Why does the timer continue to fire?
Is the timer being GC'ed? Why/why not?
If not, who has a pointer to the timer?

The closest question I found was this one, but the answers say that a System.Threading.Timer should be GC'ed in these circumstances. I am finding that it is not being GC'ed.

Here is the ThreadSafe class referenced earlier. I added it at the end because it is not relevant to the question.

//basic threadsafe object implementation. Not necessary for a slow interval, but good for peace of mind
class ThreadSafe<T>
{
    private T _value;
    private readonly object _lock = new();
    public T Value
    {
        get
        {
            lock (_lock)
            {
                return _value;
            }
        }
        set
        {
            lock (_lock)
            {
                _value = value;
            }
        }
    }
}

CodePudding user response:

When you create a Timer, internally a TimerQueueTimer is created based on your Timer. The Timer has a reference to this so that you can continue to modify and control the timer.

The static field s_queue of the System.Threading.TimerQueue class (Source) holds a reference to an active TimerQueueTimer (it's indirect, there's yet another wrapper class) even after you forget the reference to the Timer which created it.

If you examine the source in that linked file for the Timer constructor and its change method you'll see that during change a reference does get stored in TimerQueue.s_queue.

Edit: Fixed for accuracy about which objects are actually held.

CodePudding user response:

The System.Threading.Timer is a managed wrapper around some unmanaged timer.
Without call to Dispose an instance of a managed timer is GC'ed, but its unmanaged resources are kept alive.

  • Related