Reading this article, I came across the following statement:
❌ BAD This example uses Task.Delay(-1, token)
to create a Task
that completes when the CancellationToken
fires, but if it doesn’t fire, there’s no way to dispose of the CancellationTokenRegistration
created inside of Task.Delay
. This can lead to a memory leak.
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
// There's no way to dispose of the registration
var delayTask = Task.Delay(-1, cancellationToken);
var resultTask = await Task.WhenAny(task, delayTask);
if (resultTask == delayTask)
{
// Operation cancelled
throw new OperationCanceledException();
}
return await task;
}
This would seem to suggest that internally Task.Delay
registers a callback with the CancellationTokenSource
. And indeed, if you follow the calls into decompiled code you will notice that inside the Task we call:
token.UnsafeRegister(static (state, cancellationToken) =>{...}
following this code further we see that we call:
private CancellationTokenRegistration Register(Delegate callback, object? state, bool useSynchronizationContext, bool useExecutionContext)
{
...
}
with both bool
s set to false.
So this would seem to suggest that calling Task.Delay
registers the task with the CancellationTokenSource
, and if you have a long-lived CancellationTokenSource
and a short lived Task
, that task will be held on to?
Notice, we can follow nearly the same path with Task.Run(Action, CancellationToken)
thus leading to the same question, why would passing in the CancellationToken
into Task.Run
register the Task
with the CancellationTokenSource
? And could this be a source of memory leaks where we have short lived Tasks
that get passed long lived CancellationTokens
?