Arnold Zahrneinder
Arnold Zahrneinder

Reputation: 5200

Garbage collector and non-disposable types

Give the following code:

public static void Test() {
    new Timer((x)=> {
       Console.WriteLine(DateTime.Now);
    }, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
}
public static async Task Main(string[] args) {
   Test();
   await Task.Delay(10000);
   GC.Collect();
   GC.WaitForPendingFinalizers();
   GC.Collect();
   await Process.GetCurrentProcess().WaitForExitAsync();
}

Once the GC.Collect() hits, we can see that the Timer is collected and it stops working since it implement IDisposable. But if I replace the Timer with a Task, let's say:

public static void Test() {
    Task.Run(async () => {
        while(true) {
           Console.WriteLine(DateTime.Now);
           await Task.Delay(1000);
        }
    });
}

We can see that the Task keeps running although it's reference is not kept. But we know that Task does not implement IDisposable.

In both these methods I intentionally wrote a bad code for testing purpose and I did not assign the references of Timer and Task to any variables so that they would fall into the first generation (in garbage collection).

Since Task keeps running, here's a few related questions:

1- Will it be collected by Garbage Collector some time in the future?

2- Does the Garbage Collector allows the Task to run as long as the OS does not run short in memory?

I ran some test and I saw that the Task actually keeps running even for hours, but I need to know if this behavior is guaranteed to continue.

Upvotes: 0

Views: 138

Answers (1)

Gerald Mayr
Gerald Mayr

Reputation: 729

The System.Threading.Timer timer is designed to be stopped on garbage collection. Because you do not assign the timer object to any variable, it goes out of scope immediately. There are no references to the timer object (whether in your code, nor somewhere else). Therefore, it is eligible to garbage collection.

See https://github.com/microsoft/referencesource/blob/master/mscorlib/system/threading/timer.cs for the implementation. The Timer class creates a TimerHolder object internally. TimerHolder itself has a finalizer (~TimerHolder) which finally "closes" the timer once the GC runs the finalizer.

Be aware, that GC is not deterministic, in other scenarios the timer maybe will run longer (depending on how long your object has already been referenced, it may be moved to higher GC generations).


The Task class, respectively the entire asynchronous programming API is much more complex than it seems in the first place. There is a lot of work and plumbing happening in the background. See https://learn.microsoft.com/en-us/dotnet/standard/async.

A Task is typically handled by a TaskScheduler, which is associated with your current thread and usually also with other threads. Therefore, a reference to the task object exists somewhere, which prevents the task from being garbage-collected. At least until the task is completed. This is a very simplified explanation.


Edit (after some more research):

A task created using Task.Delay is not handled by the TaskScheduler (the task itself has no active work, it is just waiting, so it would make no sense to schedule it).

Task.Delay internally also uses System.Threading.Timers. But the timer is explicitly kept running, by simply suppressing finalization using GC.SuppressFinalize on the internal TimerHolder object.

see https://github.com/microsoft/referencesource/blob/master/mscorlib/system/threading/Tasks/Task.cs


As you may notice, this has nothing to do with implementation of IDisposable.

Upvotes: 4

Related Questions