Skip to content

Awaited tasks are kept in memory even after completion #34239

@DomantasJ

Description

@DomantasJ

Version Used:

Compiler version: '2.8.3.63029 (e9a3a6c)'.
Reproduced running on .net core 2.1.302 and .net framework 4.0.30319.42000 (windows 10 64bit).

Steps to Reproduce:

class Program
{
    public static void Main(string[] args)
    {
        var tasks = new List<Task>();
        for (int i = 0; i < 10; i++)
        {
            tasks.Add(Task.Run(RunAndBlock));
            Thread.Sleep(TimeSpan.FromSeconds(5));
        }

        Thread.Sleep(TimeSpan.FromDays(1));
    }

    static async Task DoWork()
    {
        var data = Enumerable.Range(0, 100_000_000 / 4).ToArray();
        await Task.Yield();
        Console.WriteLine($"Length: {data.Length}");
    }

    static async Task RunAndBlock()
    {
        Console.WriteLine($"Started task");
        await DoWork();
        // DoWork() has finished, so I would expect any local
        // variables used by that task to be no longer needed
        // await Task.Yield(); // uncomment this to prevent leak
        GC.Collect();
        Console.WriteLine($"Task and gc completed");
        Thread.Sleep(TimeSpan.FromDays(1));
    }
}

With every started task program consumes additional 100 megabytes of memory, even though each task's await DoWork() and gc completes before starting next task.

Expected Behavior:

Peak memory usage is around 100 megabytes.

Actual Behavior:

Every 5 seconds memory usage grows by 100 megabytes.

Other observations:

Here's the generated code for RunAndBlock state machine (slightly cleaned up):

void IAsyncStateMachine.MoveNext()
{
    int num1 = this.1__state;
    try
    {
        TaskAwaiter awaiter;
        int num2;
        if (num1 != 0)
        {
            Console.WriteLine("Started task");
            awaiter = Program.DoWork().GetAwaiter();
            if (!awaiter.IsCompleted)
            {
                this.1__state = num2 = 0;
                this.u__1 = awaiter;
                this.t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.<RunAndBlock>d__2>(ref awaiter, ref this);
                return;
            }
        }
        else
        {
            awaiter = this.u__1;
            this.u__1 = new TaskAwaiter();
            this.1__state = num2 = -1;
        }
        awaiter.GetResult();
        GC.Collect();
        Console.WriteLine("Task and gc completed");
        Thread.Sleep(TimeSpan.FromDays(1.0));
    }
    catch (Exception ex)
    {
        this.1__state = -2;
        this.t__builder.SetException(ex);
        return;
    }
    this.1__state = -2;
    this.t__builder.SetResult();
}

The problem seems to be that awaiter is kept on the stack after it is no longer needed (after awaiter.GetResult() call). Awaiter has a reference to the task which has a reference to the state machine of DoWork, which is why gc cannot reclaim the array. Any other await after await DoWork() causes state machine to yield, therefore dropping all values the were only kept on the stack - which is why adding await Task.Yield() fixes the leak.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area-CompilersFeature Requesthelp wantedThe issue is "up for grabs" - add a comment if you are interested in working on it

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions