Home > Net >  Memory leak when doing fire-and-forget of Task.Delay with CancellationToken
Memory leak when doing fire-and-forget of Task.Delay with CancellationToken

Time:03-14

In my application I need to schedule executing of some Actions with delay. It's like setTimeout in JavaScript. Also, when app execution ends I need to cancel all scheduled executions that have not been executed yet. So I have to call Task.Delay without await and pass CancellationToken into it. But if I do so, I face with memory leak: none of CancellationTokenSource CallbackNode will be disposed until I call Cancel and Dispose of CancellationTokenSource from which I take CancellationTokens to pass to Task.Delay.

Minimal reproducible example:

CancellationTokenSource cts = new CancellationTokenSource();
for (int i = 0; i < 1000; i  )
{
    Task.Delay(500, cts.Token).ContinueWith(_ => Console.WriteLine("Scheduled action"));
}
await Task.Delay(1000);
Console.ReadLine();

After executing this example, it leave 1000 of CancellationTokenSource CallbackNode. with leak

If I write cts.Cancel() after await Task.Delay(1000); leak does not appear without leak

Why this leak happens? All Tasks was completed, so there should not be references to cts.Token. Disposing passed Task to continuation action does not help.
Also, if I await Task that schedule execution of action, leak does not appear.

CodePudding user response:

I was a bit surprised, but it looks like this is on purpose. When a registration is cancelled, CancellationTokenSource still keeps the instance of CancellationTokenSource CallbackNode in a free-list to reuse it for the next callback. This is an opiniated optimization, that can backfire in your scenario.

To illustrate this, try running your test twice in a row:

var tasks = new Task[1000];

for (int i = 0; i < 1000; i  )
{
    tasks[i] = Task.Delay(500, cts.Token).ContinueWith(_ => Console.WriteLine("Scheduled action"));
}
await Task.WhenAll(tasks);

for (int i = 0; i < 1000; i  )
{
    tasks[i] = Task.Delay(500, cts.Token).ContinueWith(_ => Console.WriteLine("Scheduled action"));
}
await Task.WhenAll(tasks);

Console.ReadLine()

You will see that there are still 1000 instances of CancellationTokenSource CallbackNode, despite registering 2000 callbacks in total. That's because the second iteration reused the nodes created during the first one.

Not much you can do about this, I believe this is by design. In any case, the amount of memory should be mostly negligible (at most x instances, where x is the number of simultaneously registered callbacks).

  • Related