0

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 = 0;
        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(()=>Interlocked.Increment(ref count),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}");

        //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 < (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.

Nigel
  • 2,961
  • 1
  • 14
  • 32
  • 1
    `Dispose()` it. – rfmodulator Dec 07 '21 at 00:21
  • @rfmodulator of course that works, if my goal were to stop the timer. But that's not point of the question. I want to figure out why the timer isn't getting GC'ed – Nigel Dec 07 '21 at 00:24
  • 1
    @rfmodulator's comment does answer the question, no? The docs say you must dispose of something for the GC to know it can be purged. [Dispose](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.component.dispose?view=net-6.0#System_ComponentModel_Component_Dispose) – quaabaam Dec 07 '21 at 00:26
  • If you don't Dispose it, it's going to need to be Finalized by the GC. Try calling GC.WaitForPendingFinalizers after your call to Collect. But, Dispose it too – Flydog57 Dec 07 '21 at 00:28
  • @quaabaam Actually no. If you read the documentation more carefully you will see that `Dispose` frees up the resources *used by* the disposable object, not the object itself – Nigel Dec 07 '21 at 00:28
  • @TheGeneral no I wrote the code to test if I could let a timer run without anything pointing to it. My understanding is that the GC will collect if nothing points to an object. So who points to the timer? – Nigel Dec 07 '21 at 00:30
  • @Flydog57 That didn't work – Nigel Dec 07 '21 at 00:30
  • GC.KeepAlive(Object) – TheGeneral Dec 07 '21 at 00:31
  • 1
    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. – Dmitry Dec 07 '21 at 00:33
  • @TheGeneral Thats a useful tool. Didn't know about that. Doesn't answer my question though. Maybe a better way to phrase the question is "Why does it work even though I didn't use GC.KeepAlive on the timer?" – Nigel Dec 07 '21 at 00:33
  • @Dmitry That makes sense. Interesting. Post as answer and I will select it – Nigel Dec 07 '21 at 00:34
  • An OS resource like a Timer (or a File Handle, or...) uses resources other than memory. The class' author should release those in a Dispose method (marking the object as no longer needing finalization) and must release them in a Finalizer if needed. GC.Collect doesn't Invoke a collection, it requests one. If you call WaitForPendingFinalizers, then your program will block until the GC happens and any required finalizers are run – Flydog57 Dec 07 '21 at 00:35

2 Answers2

4

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 to a TimerQueueTimer (indirectly, the queue is of another wrapper class) does get stored in TimerQueue.s_queue.

Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
moreON
  • 1,977
  • 17
  • 19
  • I'm using .NET 6 so that source code is not valid. I've been using reflection to look for a reference to the `Timer` in one of the `TimerQueue` instances and haven't had any success. Do you know who points to the `Timer` in .NET 6? – Nigel Dec 07 '21 at 19:03
  • @NigelBess - The `Timer` itself doesn't really do anything related to timing - it just create another object defined internally and forwards everything you do with it to that. So GCing the `Timer` instance doesn't remove that other object unless it also has no other references remaining. I assume this implementation hasn't changed significantly in newer versions of .NET (but it could have). – moreON Dec 07 '21 at 22:41
1

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.

Dmitry
  • 13,797
  • 6
  • 32
  • 48