В следующей программе я ожидаю, что задача получит 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);
}
