Memory leak caused by C#Timer

There are three timers in C#and the timers in System.Windows.Forms and System.Timers.Timer work exactly the same way, so here we will only discuss System.Timers.Timer and System.Threading.Timer

1. Timer activation

Let's start with an example:

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

        GC.Collect();
        Read();
    }

    static void Start()
    {
        Foo f = new Foo();
        System.Threading.Thread.Sleep(5_000);
    }
}

public class Foo
{
    System.Timers.Timer _timer;

    public Foo()
    {
        _timer = new System.Timers.Timer(1000);
        _timer.Elapsed += timer_Elapsed;
        _timer.Start();
    }

    private void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
        WriteLine("System.Timers.Timer Elapsed.");
    }
    
    ~Foo()
    {
        WriteLine("---------- End ----------");
    }
}

The results are as follows:

System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
...

At the end of the Start method, the Foo instance has lost scope and should theoretically be recycled, but it did not (since the destructor was not executed, it is certain that the instance was not recycled).

This is the timer's lifesaving mechanism, because the timer needs to execute the timer_Elapsed method, which belongs to the Foo instance, so the Foo instance is saved.

But most of the time this is not what we want. The result is a memory leak. The solution is: Dispose the timer first.

public class Foo : IDisposable
{
    ...
    public void Dispose()
    {
        _timer.Dispose();
    }
}

A good guideline is that if any field in a class has an object that implements the IDisposable interface, the class should also implement the IDisposable interface.

In this example, more than the Dispose method, the Stop method, and setting AutoReset = false all release objects.However, if the Start method is called after the Stop method, the object is still alive and cannot be recycled even if the Stop method is followed by a forced garbage collection.

System.Timers.Timer and System.Threading.Timer are similar in their lifestyles.

The lifesaving mechanism is because the timer references a method in the instance, so what if the timer does not reference a method in the instance?

2. Differences between System.Timers.Timer and System.Threading.Timer without surviving

It's also easy to eliminate the timer reference to the instance method, so just change the timer_Elapsed method to static.(Static methods belong to classes, not instances.)

Run the example again after changing to a static method, as follows:

System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
---------- End ----------
System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
...

The Foo instance was destroyed (the destructor was run and End was printed), but the timer was still executing. Why?

This is because the.NET Framework ensures that System.Timers.Timer survives even if the instance it belongs to has been destroyed and recycled.

What happens if you change to System.Threading.Timer?

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

        GC.Collect();
        Read();
    }

    static void Start()
    {
        Foo2 f2 = new Foo2();
        System.Threading.Thread.Sleep(5_000);
    }
}

public class Foo2
{
    System.Threading.Timer _timer;

    public Foo2()
    {
        _timer = new System.Threading.Timer(timerTick, null, 0, 1000);
    }

    static void timerTick(object state)
    {
        WriteLine("System.Threading.Timer Elapsed.");
    }

    ~Foo2()
    {
        WriteLine("---------- End ----------");
    }
}

Note that the timerTick method here is static.The results are as follows:

System.Threading.Timer Elapsed.
System.Threading.Timer Elapsed.
System.Threading.Timer Elapsed.
System.Threading.Timer Elapsed.
System.Threading.Timer Elapsed.
---------- End ----------

As the Foo2 instance is destroyed, _timer automatically stops and destroys.

This is because the.NET Framework does not save a reference to the active System.Threading.Timer, but directly references the callback delegate.

Tags: C# Windows

Posted on Wed, 05 Feb 2020 23:08:09 -0500 by mahlia