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.