Description
I have encountered an issue a specific instance of my code leaks when the .NET Core runtime is upgraded from 2.0 to 2.1. While the original code is too complex to post, I was able to reproduce the issue with a distilled example.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace MemoryTester
{
class Program
{
static void Main(string[] args)
{
List<Task<long>> tasks = new List<Task<long>>();
for (var i = 0; i < 2000; i++)
{
var task = MockWork(new byte[1024 * 1024]);
tasks.Add(task);
task.Wait();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true);
GC.WaitForPendingFinalizers();
}
var all = Task.WhenAll(tasks).GetAwaiter().GetResult();
Console.WriteLine($"Finished working on {all.Sum()} bytes.");
}
static async Task<long> MockWork(byte[] bytes)
{
await Task.Delay(10);
await Task.Yield();
return bytes.Length;
}
}
}
I can see how it would leak, if MockWork
is compiled into a state machine with a property representing bytes
, then the fact that Main
holds a reference to all the tasks would prevent it from garbage collecting those byte arrays. However, I find it curious that this style of code works in 2.0 which leads me to believe that it nulls out unused fields in the state machine when the task is complete.
All in all, my question is this leak expected behavior?
For what it's worth, I suspect that the change in behavior may be related to these two PRs for 2.1:
dotnet/coreclr#13105, dotnet/coreclr#14178