In my application I need to schedule executing of some Action
s 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 CancellationToken
s 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
.
If I write cts.Cancel()
after await Task.Delay(1000);
leak does not appear
Why this leak happens? All Task
s 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).