Reputation: 5200
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
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