83

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();
  }
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 1
    Why is `timer1` even garbage-collected? Isn't it still in scope? – Jeff Sternal Feb 10 '11 at 20:31
  • 3
    Jeff: Scope isn't really relevant. This is pretty much the raison-d'être for the GC.KeepAlive method. If you're interested in the picky details, see http://blogs.msdn.com/b/cbrumme/archive/2003/04/19/51365.aspx. – Nicole Calinoiu Feb 10 '11 at 20:42
  • 7
    Take a look with Reflector at the Timer.Enabled setter. Note the trick it uses with "cookie" to give the system timer a state object to use in the callback. The CLR is aware of it, clr/src/vm/comthreadpool.cpp, CorCreateTimer() in the SSCLI20 source code. MakeDelegateInfo() gets complicated. – Hans Passant Feb 10 '11 at 20:45
  • @Nicole: mighty interesting! Thanks for the link. That raises another question though: when does a `System.Threading.Timer` instance become eligible for garbage collection in the first place? Moreover, if we're to understand that the call to `GC.Collect()` in the question actually collects the timer (and we don't have to wait for the program execution to end), how is the class even usable? – Jeff Sternal Feb 10 '11 at 20:58
  • This is curious: if you turn off optimizations and specify that output will contain full debug info (getting yourself most of the way to a Debug build), `timer1` is not garbage collected. – Jeff Sternal Feb 10 '11 at 21:49
  • 1
    @Jeff: That's expected behavior; details in [CLR via C#](http://tinyurl.com/3alejm9) and mentioned [on my blog](http://nitoprograms.blogspot.com/2010/02/q-should-i-set-variables-to-null-to.html) under "When the JIT Compiler Behaves Differently (Debug)". – Stephen Cleary Feb 10 '11 at 22:38
  • Why is timer1 garbage-collected? Isn't the native timer holding a reference to the object? – John Feb 11 '11 at 04:11
  • @John: Native code can't hold references. When .NET code needs to keep a reference alive so native code can use it later, it normally uses special mechanisms such as `GCHandle`. In this case, I belive Hans is on the right track - this is something of a CLR special case (an undocumented one, at that). – Stephen Cleary Feb 11 '11 at 23:56
  • 2
    @StephenCleary wow - mental. I just found a bug in an app revolving around System.Timers.Timer staying alive and publishing updates after I'd expect it to die. Thanks for saving a lot of time! – Dr. Andrew Burnett-Thompson Jan 26 '12 at 15:12
  • @JeffSternal A bit late, but... Scope isn't used to determine lifetime in C# - the object is eligible for collection as soon as it's used at no further point in the method. In other words, doing the usual `data = null;` is pointless and misleading. How to use the timer, then? You have to root it somehow. For example, use a field instead of a variable. Or have a reference to that timer preserved - e.g. by adding `GC.KeepAlive(timer);` to the end of this sample, or by referencing the timer in the callback, for example. – Luaan Sep 09 '14 at 12:43
  • 2
    So, if you always call timer1.Change(dueTime, period) yourself after the constructor, you won't be GC'd by surprise. – fastmultiplication Nov 08 '17 at 19:44
  • 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:32

4 Answers4

35

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)

John
  • 5,561
  • 1
  • 23
  • 39
13

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.

Nick H
  • 154
  • 1
  • 4
  • It actually accords with the documentation. If you keep a reference to System.Threading.Timer in the callback(via closure or constructor), then it won't be collected like any managed object. – joe Jun 29 '20 at 05:01
  • 1
    @joe Do you have a reference in the docs cause I can't seem to find this mentioned on the main Timer or TimerCallback page? – user3797758 Jan 19 '21 at 19:14
  • @user3797758 The [docs](https://learn.microsoft.com/en-us/dotnet/api/system.threading.timer?view=net-5.0) doesn't mention it directly, but it's aligned with this anwser. see the `Remarks` part _As long as you are using a Timer, you must keep a reference to it. As with any managed object, a Timer is subject to garbage collection when there are no references to it. The fact that a Timer is still active does not prevent it from being collected._ Here in this case, the static `TimerQueue` keeps a reference to the timer instance, so it won't be collected. – joe Jan 20 '21 at 02:33
  • 1
    Yeah I read that part too but that's not any different from any other C# object, I thought you meant something else with your comment... – user3797758 Jan 20 '21 at 10:46
1

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.

Andy
  • 8,432
  • 6
  • 38
  • 76
  • 3
    The event handler does not have a reference to the `Program` class, and even if it did, it wouldn't prevent the timer from being GC'ed. – Stephen Cleary Feb 13 '11 at 18:28
  • 3
    Ya it does. Compile the above code and then look at it with .Net reflector. The += lamba is converted to a method in the Program class. And yes, event handlers being linked DO prevent garbage collection. http://blogs.msdn.com/b/abhinaba/archive/2009/05/05/memory-leak-via-event-handlers.aspx – Andy Mar 07 '11 at 23:59
  • Working link for the above dead link by Andy: [Memory leak via event handlers, Abhinaba Basu](https://blog.bonggeek.com/2009/05/memory-leak-via-event-handlers.html). – Theodor Zoulias Aug 28 '23 at 04:27
0

FYI, as of .NET 4.6 (if not earlier), this appears to not be true anymore. Your test program, when run today, does not result in either timer being garbage collected.

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

As I look at the implementation of System.Threading.Timer, this seems to make sense as it appears that the current version of .NET uses linked list of active timer objects and that linked list is held by a member variable inside TimerQueue (which is a singleton object kept alive by a static member variable also in TimerQueue). As a result, all timer instances will be kept alive as long as they are active.

Erv Walter
  • 13,737
  • 8
  • 44
  • 57
  • 3
    I am still seeing the `System.Threading.Timer` instance being collected in .NET 4.6. Please make sure you are compiling the code in release mode with optimizations enabled. The linked list you mentioned contains helper `TimerQueueTimer` objects; it does not prevent the original `System.Threading.Timer` instance from being collected by the GC. (Each `System.Threading.Timer` instance references its own `TimerQueueTimer` object, but not vice versa. When the `System.Threading.Timer` is collected by the GC, its `TimerQueueTimer` object is removed from the queue by the `~TimerHolder` finalizer.) – Antosha May 02 '16 at 23:26
  • Try it in a release mode. – Ievgen Mar 21 '18 at 16:05
  • Same here, running the code on .NET Core 3.0. None of the two timers is garbage collected, in either Debug or Release build. To reproduce the issue I must move the creation of the timers outside of the `Main` method. – Theodor Zoulias Mar 27 '20 at 12:23