29

When you use a Timer or a Thread that will just run for the entire lifetime of the program do you need to keep a reference to them to prevent them from being garbage collected?

Please put aside the fact that the below program could have timer as a static variable in the class, this is just a toy example to show the issue.

public class Program
{
    static void Main(string[] args)
    {
        CreateTimer();

        Console.ReadLine();
    }

    private static void CreateTimer()
    {
        var program = new Program();

        var timer = new System.Timers.Timer();
        timer.Elapsed += program.TimerElapsed;
        timer.Interval = 30000;
        timer.AutoReset = false;
        timer.Enabled = true;
    }

    private void TimerElapsed(object sender, ElapsedEventArgs e)
    {
        var timerCast = (Timer)sender;

        Console.WriteLine("Timer fired at in thread {0}", GetCurrentThreadId());

        timerCast.Enabled = true;
    }

    ~Program()
    {
        Console.WriteLine("Program Finalized");
    }

    [DllImport("kernel32.dll")]
    static extern uint GetCurrentThreadId();
}

Could the timer get collected in that above example? I ran it for a while and I never got a exception nor a message saying ~Program() was called.

UPDATE: I found out from this question (thanks sethcran) that threads are tracked by the CLR, but I still would like an answer about Timers.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
  • 2
    Which `Timer` type are you using here? WinForms, Threading, etc ... – JaredPar Aug 08 '13 at 21:50
  • @JaredPar Any of them (but I used `System.Timers` in the above example). I am also curious if I need to keep a reference to any Threads I create using `Thread` – Scott Chamberlain Aug 08 '13 at 21:52
  • I'm not sure what you mean here. The Timer classes (Windows.Forms and System.Timers) both use a thread for the duration (until manually disposed or reference destroyed). In light of this, the GC is done as for threads which is described in the link provided above... – MoonKnight Aug 08 '13 at 21:59
  • Based on the code in the example, the timer is just pulling a thread from the thread pool. [Here's the documentation](http://msdn.microsoft.com/en-us/library/System.Timers.Timer.aspx). – Shaz Aug 08 '13 at 22:02
  • 2
    @Killercam: `System.Threading.Timer` (on which `System.Timers.Timer` is based) does not use a thread for the entire duration. It only uses a threadpool thread when the timer elapses, to run the callback. Then the thread is recycled to the pool. – Jim Mischel Aug 08 '13 at 22:08
  • 1
    Does this answer your question? [Why does a System.Timers.Timer survive GC but not System.Threading.Timer?](https://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer) – Michael Freidgeim Nov 22 '20 at 09:01

3 Answers3

56

This is only a problem with the System.Threading.Timer class if you don't otherwise store a reference to it somewhere. It has several constructor overloads, the ones that take the state object are important. The CLR pays attention to that state object. As long as it is referenced somewhere, the CLR keeps the timer in its timer queue and the timer object won't get garbage collected. Most programmers will not use that state object, the MSDN article certainly doesn't explain its role.

System.Timers.Timer is a wrapper for the System.Threading.Timer class, making it easier to use. In particular, it will use that state object and keep a reference to it as long as the timer is enabled.

Note that in your case, the timer's Enabled property is false when it enters your Elapsed event handler because you have AutoReset = false. So the timer is eligible for collection as soon as it enters your event handler. But you stay out of trouble by referencing the sender argument, required to set Enabled back to true. Which makes the jitter report the reference so you don't have a problem.

Do be careful with the Elapsed event handler. Any exception thrown inside that method will be swallowed without a diagnostic. Which also means that you won't set Enabled back to true. You must use try/catch to do something reasonable. If you are not going to intentionally end your program, at a minimum you'll need to let your main program know that something isn't working anymore. Putting Enabled = true in a finally clause can avoid getting the timer garbage collected, but at the risk of having your program throw exceptions over and over again.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • 3
    +1. Especially liked you mentioning the crazy exception squashing, which is the primary reason I won't use `System.Timers.Timer`. – Jim Mischel Aug 09 '13 at 02:28
  • If I use a Threading.Timer instance with not null state for **single** callback calling (period is `Timeout.InfiniteTimeSpan`) then can GC collect the timer after calling without any troubles? – Ilya Loskutov Dec 24 '16 at 23:54
  • If my `System.Timers.Timer` is not a member and i just start it in a method it works fine as u mentioned. But do I have to stop it or does it goes well with the garbage collection? – Peter Sep 25 '17 at 12:04
  • It can only be GC-ed after you stop it. – Hans Passant Sep 25 '17 at 12:08
  • What about `System.Windows.Forms.Timer`? – Sweeper Jan 10 '19 at 07:24
5

Let's carry out an experiment:

private static void UnderTest()
{
    // Timer is a local variable; its callback is local as well
    System.Threading.Timer timer = new System.Threading.Timer(
        (s) => { MessageBox.Show("Timer!"); }, 
        null, 
        1000, 
        1000);
}

...

// Let's perform Garbage Collection manually: 
// we don't want any surprises 
// (e.g. system starting collection in the middle of UnderTest() execution)
GC.Collect(2, GCCollectionMode.Forced);

UnderTest();

// To delay garbage collection
// Thread.Sleep(1500);

// To perform Garbage Collection
// GC.Collect(2, GCCollectionMode.Forced);

So far

  • if we keep commented both Thread.Sleep(1500); and GC.Collect(2, GCCollectionMode.Forced); we'll see message boxes appear: the timer is working
  • if we uncomment GC.Collect(2, GCCollectionMode.Forced); we'll see nothing: the timer starts then it is collected
  • if we uncomment both Thread.Sleep(1500); and GC.Collect(2, GCCollectionMode.Forced); we'll see a single message box: the timer starts, goes off a single message box and then the timer is collected

So System.Threading.Timers are collected as any other object instances.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Dmitry Bychenko
  • 180,369
  • 20
  • 160
  • 215
3

Add this code to a program and run it. You'll see that the timer is NOT collected.

    private void DoStuff()
    {
        CreateTimer();
        Console.WriteLine("Timer started");
        int count = 0;
        for (int x = 0; x < 1000000; ++x)
        {
            string s = new string("just trying to exercise the garbage collector".Reverse().ToArray());
            count += s.Length;
        }
        Console.WriteLine(count);
        Console.Write("Press Enter when done:");
        Console.ReadLine();
    }

    private void Ticktock(object s, System.Timers.ElapsedEventArgs e)
    {
        Console.WriteLine("Ticktock");
    }

    private void CreateTimer()
    {
        System.Timers.Timer t = new System.Timers.Timer(); // Timer(Ticktock, null, 1000, 1000);
        t.Elapsed += Ticktock;
        t.Interval = 1000;
        t.AutoReset = true;
        t.Enabled = true;
    }

So the answer to your question appears to be that the timer is not eligible for collection and will not be collected if you don't maintain a reference to it.

It's interesting to note that if you run the same test with System.Threading.Timer, you'll find that the timer is collected.

Jim Mischel
  • 131,090
  • 20
  • 188
  • 351
  • @HansPassant: Thanks. I re-ran the test with `System.Timers.Timer` and found that it's not collected. Interesting. – Jim Mischel Aug 08 '13 at 22:25
  • There is no indication that a GC even occurred. It is the GC of objects (tracked internally by weak handles or that 'Dispose' in their finalizes) that is of interest. Being *eligible* for reclamation is not sufficient. – user2864740 Jul 14 '18 at 18:57