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.