Why does a System.Timers.Timer survive GC but not System.Threading.Timer?

You can answer this and similar questions with windbg, sos, and !gcroot

0:008> !gcroot -nostacks 0000000002354160
DOMAIN(00000000002FE6A0):HANDLE(Strong):241320:Root:00000000023541a8(System.Thre
ading._TimerCallback)->
00000000023540c8(System.Threading.TimerCallback)->
0000000002354050(System.Timers.Timer)->
0000000002354160(System.Threading.Timer)
0:008>

In both cases, the native timer has to prevent GC of the callback object (via a GCHandle). The difference is that in the case of System.Timers.Timer the callback references the System.Timers.Timer object (which is implemented internally using a System.Threading.Timer)


I have been googling this issue recently after looking at some example implementations of Task.Delay and doing some experiments.

It turns out that whether or not System.Threading.Timer is GCd depends on how you construct it!!!

If constructed with just a callback then the state object will be the timer itself and this will prevent it from being GC'd. This does not appear to be documented anywhere and yet without it it is extremely difficult to create fire and forget timers.

I found this from the code at http://www.dotnetframework.org/default.aspx/DotNET/DotNET/8@0/untmp/whidbey/REDBITS/ndp/clr/src/BCL/System/Threading/Timer@cs/1/Timer@cs

The comments in this code also indicate why it is always better to use the callback-only ctor if the callback references the timer object returned by new as otherwise there could be a race bug.


In timer1 you're giving it a callback. In timer2 to you're hooking up an event handler; this setups up a reference to your Program class which means the timer won't be GCed. Since you never use the value of timer1 again, (basically the same as if you removed the var timer1 = ) the compiler is smart enough to optimize away the variable. When you hit the GC call, nothing is referencing timer1 anymore so its' collected.

Add a Console.Writeline after your GC call to output one of the properties of timer1 and you'll notice it's not collected anymore.