В следующей программе я ожидаю, что задача получит GC'd, но это не так. Я использовал профилировщик памяти, который показал, что CancellationTokenSource
содержит ссылку на него, хотя задача явно находится в конечном состоянии. Если я удалю TaskContinuationOptions.OnlyOnRanToCompletion
, все будет работать так, как ожидалось.
Почему это происходит и что я могу сделать, чтобы предотвратить его?
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);
}
Мое подозрение в том, что, поскольку продолжение никогда не запускается (оно не соответствует критериям, указанным в его опциях), оно никогда не отменяет токен отмены. Таким образом, CTS ссылается на продолжение, в котором содержится ссылка на первую задачу.
Обновление
Команда PFX подтвердила, что это, по-видимому, является утечкой. В качестве обходного пути мы прекратили использовать любые условия продолжения при использовании токенов отмены. Вместо этого мы всегда выполняем продолжение, проверяем условие внутри и бросаем OperationCanceledException
, если он не выполняется. Это сохраняет семантику продолжения. Следующий способ расширения инкапсулирует это:
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);
}