See following example:
public static void ForgottenTask()
{
Action<object> action = (object obj) =>
{
Console.WriteLine("Task={0}, obj={1}, Thread={2}", Task.CurrentId, obj, Thread.CurrentThread.ManagedThreadId);
};
new Task(action, "alpha").ContinueWith(action);
}
static void Main(string[] args)
{
for (int i = 0; i < 1000000; i++)
ForgottenTask();
GC.Collect();
GC.Collect();
Debugger.Break();
}
Obviously no action is ever executed and that is expected. What is strange is that when I check tasks during Debugger.Break via menu -> Debug -> Windows > Tasks/Parallel Stacks (in Visual Studio 2022; I don't know any easier way), I see 10 000 of them in 'Scheduled' state. I am not sure if it is debugging limit or somewhat limit of scheduler. So there is my first question, why 10 000?
Anyway the tasks are not garbage collected which could be kind of expected since they have reference in TaskScheduler. But my question is what will happen with them? Will they hang there forever (sounds like memory leak)? Or they will be somehow reused/removed? If that is so, when and how?
I used .NET 6 and VS 2022 in the example (if that is relevant)
I don't know much about the implications of using the debugging features of the Visual Studio, but if you run your program without the debugger attached (with Ctrl+F5) the tasks are properly recycled by the garbage collector. The tasks created internally by the ForgottenTask method are not started, so they are not scheduled, and since you are not holding any explicit reference to the tasks there is nothing preventing the garbage collector from recycling them. Here is a minimal demonstration of this behavior:
using System;
using System.Threading.Tasks;
public class Program
{
public static void Main()
{
var weakReference = ForgottenTask();
Console.WriteLine($"Before GC.Collect, IsAlive: {weakReference.IsAlive}");
GC.Collect();
Console.WriteLine($"After GC.Collect, IsAlive: {weakReference.IsAlive}");
}
static WeakReference ForgottenTask()
{
var task = new Task(() => { }).ContinueWith(_ => { });
return new WeakReference(task);
}
}
Output:
Before GC.Collect, IsAlive: True
After GC.Collect, IsAlive: False