Zadanie nie jest zbierane
W poniższym programie oczekuję, że zadanie uzyska GC'd, ale tak nie jest. Użyłem profilera pamięci, który pokazał, żeCancellationTokenSource
zawiera odniesienie do niego, mimo że zadanie jest wyraźnie w stanie ostatecznym. Jeśli usunęTaskContinuationOptions.OnlyOnRanToCompletion
, wszystko działa zgodnie z oczekiwaniami.
Dlaczego tak się dzieje i co mogę zrobić, aby temu zapobiec?
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);
}
Podejrzewam, że ponieważ kontynuacja nigdy nie działa (nie spełnia kryteriów określonych w opcjach), nigdy nie wyrejestrowuje się z tokena anulowania. Tak więc CTS zawiera odniesienie do kontynuacji, która zawiera odniesienie do pierwszego zadania.
Aktualizacja
Zespół PFX potwierdził, że jest to przeciek. Aby obejść ten problem, przestaliśmy używać wszelkich warunków kontynuacji podczas korzystania z tokenów anulowania. Zamiast tego zawsze wykonujemy kontynuację, sprawdzamy stan wewnątrz i rzucamyOperationCanceledException
jeśli nie zostanie spełniony. Zachowuje to semantykę kontynuacji. Następująca metoda rozszerzenia obejmuje to:
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);
}