9

According to MSDN, a reference to System.Threading.Timer should be kept otherwise it will get garbage-collected. So if I run this code, it doesn't write any message (which is the expected behavior):

static void Main(string[] args)
{
    RunTimer();
    GC.Collect();
    Console.ReadKey();
}

public static void RunTimer()
{
    new Timer(s => Console.WriteLine("Hello"), null, TimeSpan.FromSeconds(1), TimeSpan.Zero);
}

However, if I modify the code slightly by storing the timer in a temporary local variable, it survives and writes the message:

public static void RunTimer()
{
    var timer = new Timer(s => Console.WriteLine("Hello"));
    timer.Change(TimeSpan.FromSeconds(1), TimeSpan.Zero);
}

During garbage collection, there is apparently no way how to access the timer from root or static objects. So can you please explain why the timer survives? Where is the reference preserved?

Cody Gray - on strike
  • 239,200
  • 50
  • 490
  • 574
Peter Smolinsky
  • 248
  • 2
  • 8
  • 7
    This certainly *used* to be true. Looks like Microsoft finally did something about it, thousands of support calls must have been inspiring. Not sure when this happened, somewhere around 4.5 or 4.6 I suspect. They are now kept alive along as they are ticking, [TimerQueue.s_queue](http://referencesource.microsoft.com/#mscorlib/system/threading/timer.cs,75523a07eb2de983) takes care of it. – Hans Passant Jun 07 '16 at 14:32
  • 3
    Thanks, but then should timer be kept alive in both scenario? Why it survives only the second one? I'm running .NET 4.6.1. – Peter Smolinsky Jun 07 '16 at 14:41
  • 1
    That looks like a bug to me. Only the Change() method gets the timer added to the timer queue, the constructor forgets to do that. Pretty hard to imagine this was intentional, consider reporting it. – Hans Passant Jun 07 '16 at 14:46
  • 1
    Ok, thanks for explanation. Although I still fully don't understand it. If I capture the timer in a closure, despite using buggy constructor, it survives. Why? `public static void RunTimer() { Timer timer = null; timer = new Timer(s => Console.WriteLine("Hello {0}", timer?.GetType()), null, TimeSpan.FromSeconds(1), TimeSpan.Zero); }` – Peter Smolinsky Jun 07 '16 at 15:04
  • I don't think we are supposed to understand it, such is the way bugs behave. Just ask the gurus, post to connect.microsoft.com and report what you see. – Hans Passant Jun 07 '16 at 15:07
  • Curious. What happens if you wait for pending finalizers after the collection? – Eric Lippert Jun 07 '16 at 15:49
  • Does this answer your question? [Can Timers get automatically garbage collected?](https://stackoverflow.com/questions/18136735/can-timers-get-automatically-garbage-collected) – Michael Freidgeim Nov 20 '20 at 12:30

1 Answers1

6

Each Timer references a TimerHolder, which references a TimerQueueTimer. The implementation keeps an internal reference to the TimerQueueTimer via the call to UpdateTimer().

Under normal circumstances your timer is free to be collected, finalizing the TimerHolder and removing the TimerQueueTimer from the internal queue. But the simple constructor Timer(TimerCallback) calls TimerSetup() with the Timer itself as the state. So in this particular case, the TimerQueueTimer's state references back to the Timer, preventing it from being collected.

The effect is not related to keeping a temporary local variable. It just so happens to work due to the internals of the Timer mechanism. It is much clearer and safer to keep a reference to your timer as recommended by MSDN.

piedar
  • 2,599
  • 1
  • 25
  • 37