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.
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:
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
RunAndBlockstate machine (slightly cleaned up):The problem seems to be that
awaiteris kept on the stack after it is no longer needed (afterawaiter.GetResult()call). Awaiter has a reference to the task which has a reference to the state machine ofDoWork, which is why gc cannot reclaim the array. Any other await afterawait DoWork()causes state machine to yield, therefore dropping all values the were only kept on the stack - which is why addingawait Task.Yield()fixes the leak.