Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

It appears that System.Timers.Timer instances are kept alive by some mechanism, but System.Threading.Timer instances are not.

Sample program, with a periodic System.Threading.Timer and auto-reset System.Timers.Timer:

class Program {   static void Main(string[] args)   {     var timer1 = new System.Threading.Timer(       _ => Console.WriteLine("Stayin alive (1)..."),       null,       0,       400);      var timer2 = new System.Timers.Timer     {       Interval = 400,       AutoReset = true     };     timer2.Elapsed += (_, __) => Console.WriteLine("Stayin alive (2)...");     timer2.Enabled = true;      System.Threading.Thread.Sleep(2000);      Console.WriteLine("Invoking GC.Collect...");     GC.Collect();      Console.ReadKey();   } } 

When I run this program (.NET 4.0 Client, Release, outside the debugger), only the System.Threading.Timer is GC'ed:

Stayin alive (1)... Stayin alive (1)... Stayin alive (2)... Stayin alive (1)... Stayin alive (2)... Stayin alive (1)... Stayin alive (2)... Stayin alive (1)... Stayin alive (2)... Invoking GC.Collect... Stayin alive (2)... Stayin alive (2)... Stayin alive (2)... Stayin alive (2)... Stayin alive (2)... Stayin alive (2)... Stayin alive (2)... Stayin alive (2)... Stayin alive (2)... 

EDIT: I've accepted John's answer below, but I wanted to expound on it a bit.

When running the sample program above (with a breakpoint at Sleep), here's the state of the objects in question and the GCHandle table:

!dso OS Thread Id: 0x838 (2104) ESP/REG  Object   Name 0012F03C 00c2bee4 System.Object[]    (System.String[]) 0012F040 00c2bfb0 System.Timers.Timer 0012F17C 00c2bee4 System.Object[]    (System.String[]) 0012F184 00c2c034 System.Threading.Timer 0012F3A8 00c2bf30 System.Threading.TimerCallback 0012F3AC 00c2c008 System.Timers.ElapsedEventHandler 0012F3BC 00c2bfb0 System.Timers.Timer 0012F3C0 00c2bfb0 System.Timers.Timer 0012F3C4 00c2bfb0 System.Timers.Timer 0012F3C8 00c2bf50 System.Threading.Timer 0012F3CC 00c2bfb0 System.Timers.Timer 0012F3D0 00c2bfb0 System.Timers.Timer 0012F3D4 00c2bf50 System.Threading.Timer 0012F3D8 00c2bee4 System.Object[]    (System.String[]) 0012F4C4 00c2bee4 System.Object[]    (System.String[]) 0012F66C 00c2bee4 System.Object[]    (System.String[]) 0012F6A0 00c2bee4 System.Object[]    (System.String[])  !gcroot -nostacks 00c2bf50  !gcroot -nostacks 00c2c034 DOMAIN(0015DC38):HANDLE(Strong):9911c0:Root:  00c2c05c(System.Threading._TimerCallback)->   00c2bfe8(System.Threading.TimerCallback)->   00c2bfb0(System.Timers.Timer)->   00c2c034(System.Threading.Timer)  !gchandles GC Handle Statistics: Strong Handles:       22 Pinned Handles:       5 Async Pinned Handles: 0 Ref Count Handles:    0 Weak Long Handles:    0 Weak Short Handles:   0 Other Handles:        0 Statistics:       MT    Count    TotalSize Class Name 7aa132b4        1           12 System.Diagnostics.TraceListenerCollection 79b9f720        1           12 System.Object 79ba1c50        1           28 System.SharedStatics 79ba37a8        1           36 System.Security.PermissionSet 79baa940        2           40 System.Threading._TimerCallback 79b9ff20        1           84 System.ExecutionEngineException 79b9fed4        1           84 System.StackOverflowException 79b9fe88        1           84 System.OutOfMemoryException 79b9fd44        1           84 System.Exception 7aa131b0        2           96 System.Diagnostics.DefaultTraceListener 79ba1000        1          112 System.AppDomain 79ba0104        3          144 System.Threading.Thread 79b9ff6c        2          168 System.Threading.ThreadAbortException 79b56d60        9        17128 System.Object[] Total 27 objects 

As John pointed out in his answer, both timers register their callback (System.Threading._TimerCallback) in the GCHandle table. As Hans pointed out in his comment, the state parameter is also kept alive when this is done.

As John pointed out, the reason System.Timers.Timer is kept alive is because it is referenced by the callback (it is passed as the state parameter to the inner System.Threading.Timer); likewise, the reason our System.Threading.Timer is GC'ed is because it is not referenced by its callback.

Adding an explicit reference to timer1's callback (e.g., Console.WriteLine("Stayin alive (" + timer1.GetType().FullName + ")")) is sufficient to prevent GC.

Using the single-parameter constructor on System.Threading.Timer also works, because the timer will then reference itself as the state parameter. The following code keeps both timers alive after the GC, since they are each referenced by their callback from the GCHandle table:

class Program {   static void Main(string[] args)   {     System.Threading.Timer timer1 = null;     timer1 = new System.Threading.Timer(_ => Console.WriteLine("Stayin alive (1)..."));     timer1.Change(0, 400);      var timer2 = new System.Timers.Timer     {       Interval = 400,       AutoReset = true     };     timer2.Elapsed += (_, __) => Console.WriteLine("Stayin alive (2)...");     timer2.Enabled = true;      System.Threading.Thread.Sleep(2000);      Console.WriteLine("Invoking GC.Collect...");     GC.Collect();      Console.ReadKey();   } } 
like image 914
Stephen Cleary Avatar asked Feb 10 '11 20:02

Stephen Cleary


People also ask

Does system timers timer run in a separate thread?

Yes, they run in a different thread.

What is System threading timer?

System. Threading. Timer, which executes a single callback method on a thread pool thread at regular intervals. The callback method is defined when the timer is instantiated and cannot be changed.

Is system threading timer thread safe?

Timer is not thread-safe.

How does system timer work?

Remarks. The Timer component is a server-based timer that raises an Elapsed event in your application after the number of milliseconds in the Interval property has elapsed. You can configure the Timer object to raise the event just once or repeatedly using the AutoReset property.


2 Answers

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)

like image 110
John Avatar answered Sep 18 '22 18:09

John


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.

like image 39
Nick H Avatar answered Sep 20 '22 18:09

Nick H