In the following program, I'd expect the task to get GC'd, but it doesn't. I've used a memory profiler which showed that the CancellationTokenSource
holds a reference to it even though the task is clearly in a final state. If I remove TaskContinuationOptions.OnlyOnRanToCompletion
, everything works as expected.
Why does it happen and what can I do to prevent it?
static void Main()
{
var cts = new CancellationTokenSource();
var weakTask = Start(cts);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Console.WriteLine(weakTask.IsAlive); // prints True
GC.KeepAlive(cts);
}
private static WeakReference Start(CancellationTokenSource cts)
{
var task = Task.Factory.StartNew(() => { throw new Exception(); });
var cont = task.ContinueWith(t => { }, cts.Token, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default);
((IAsyncResult)cont).AsyncWaitHandle.WaitOne(); // prevents inlining of Task.Wait()
Console.WriteLine(task.Status); // Faulted
Console.WriteLine(cont.Status); // Canceled
return new WeakReference(task);
}
My suspicion is that because the continuation never runs (it doesn't meet the criteria specified in its options), it never unregisters from the cancellation token. So the CTS holds a reference to the continuation, which holds a reference to the first task.
Update
The PFX team has confirmed that this does appear to be a leak. As a workaround, we've stopped using any continuation conditions when using cancellation tokens. Instead, we always execute the continuation, check the condition inside, and throw an OperationCanceledException
if it is not met. This preserves the semantics of the continuation. The following extension method encapsulates this:
public static Task ContinueWith(this Task task, Func<TaskStatus, bool> predicate,
Action<Task> continuation, CancellationToken token)
{
return task.ContinueWith(t =>
{
if (predicate(t.Status))
continuation(t);
else
throw new OperationCanceledException();
}, token);
}