2

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)

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
robot40q
  • 101
  • 5
  • Please read the followings: [1](https://stackoverflow.com/questions/18091002/what-gotchas-exist-with-tasks-and-garbage-collection), [2](https://pretagteam.com/question/task-doesnt-get-garbagecollected), [3](https://www.examplefiles.net/cs/467423) – Peter Csala Dec 09 '21 at 14:25
  • BTW you should try to avoid to use the `Task` constructor. Please prefer `Task.Run` or in more [advanced scenarios](https://devblogs.microsoft.com/pfxteam/task-run-vs-task-factory-startnew/) `TaskFactory.StartNew`. – Peter Csala Dec 10 '21 at 08:29
  • 1
    @PeterCsala I think that both `Task.Run` and `Task.Factory.StartNew` would eliminate the memory leak, since the tasks would actually be executed. In this case, the task is never executed, which is why it can't be cleaned up. – David L Dec 10 '21 at 17:40
  • @PeterCsala: I read first two them and still don't know why. The third seems to be just answers without question (I feel kind of stupid but I don't see the question there). The code is just example I normally don't create tasks this way. – robot40q Dec 13 '21 at 19:52
  • @DavidL: I guess that's true but this is not my original case. I just wanted to provide complete example which is small and easy enough. My original problem occured in DataFlow library. There is this Completion property [link](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.dataflow.idataflowblock.completion?view=net-6.0) on DataflowBlock which seems to be Task based on TaskCompletionSource. This task leaks when you don't call complete on the block. But I think the root problem is the same and my example seems easier to show. – robot40q Dec 13 '21 at 20:02
  • @robot40q I don't think that's an apples to apples comparison. There is a large amount of cleanup in the DataFlowBlock.cs class, including an unlink ref, a timer, and a CancellationTokenSource that needs to be canceled: https://github.com/dotnet/corefx/blob/master/src/System.Threading.Tasks.Dataflow/src/Base/DataflowBlock.cs#L1244. It is hard to say that this is the same situation – David L Dec 13 '21 at 20:09
  • @DavidL: You may be right. I was curious why threads can hang forever. So I created example of that. In this way it is the same. I think the actual Completion Task is Continuation of TaskCompletionSource. But honestly I am not going through all that code. – robot40q Dec 13 '21 at 20:13

1 Answers1

0

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

Try it on Fiddle.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Sorry for being so slow to respond. I am currently quite busy and I want to double check your answer and if it proves right I will accept it. I wouldn't be first time debugger betrayed me, but it always comes out of nowhere as a surprise. – robot40q Dec 15 '21 at 22:15
  • @robot40q ha ha! Personally I'm rarely betrayed by the debugger, because I rarely use it. I hate my life every time I face a bug so intricate, that I have no other option than to use the debugger. :-) – Theodor Zoulias Dec 15 '21 at 22:20