Tarea no recogida de basura

En el siguiente progtwig, esperaría que la tarea obtuviera GC’d, pero no es así. He utilizado un generador de perfiles de memoria que muestra que el CancellationTokenSource mantiene una referencia a él, aunque la tarea está claramente en un estado final. Si TaskContinuationOptions.OnlyOnRanToCompletion , todo funciona como se esperaba.

¿Por qué sucede y qué puedo hacer para prevenirlo?

  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); } 

Mi sospecha es que debido a que la continuación nunca se ejecuta (no cumple con los criterios especificados en sus opciones), nunca se anula el registro del token de cancelación. Así que el CTS contiene una referencia a la continuación, que contiene una referencia a la primera tarea.

Actualizar

El equipo de PFX ha confirmado que esto parece ser una fuga. Como solución alternativa, hemos dejado de usar cualquier condición de continuación cuando usamos tokens de cancelación. En su lugar, siempre ejecutamos la continuación, verificamos la condición en el interior y lanzamos una OperationCanceledException si no se cumple. Esto preserva la semántica de la continuación. El siguiente método de extensión encapsula esto:

 public static Task ContinueWith(this Task task, Func predicate, Action continuation, CancellationToken token) { return task.ContinueWith(t => { if (predicate(t.Status)) continuation(t); else throw new OperationCanceledException(); }, token); } 

Respuesta corta: creo que esto es una pérdida de memoria (o dos, ver más abajo) y debe informarla .

Respuesta larga:

La razón por la que la Task no está GCed es porque se puede acceder desde el CTS de esta manera: ctsconttask . Creo que ambas referencias no deberían existir en su caso.

La referencia de ctscont está allí porque cont se registra correctamente para la cancelación utilizando el token, pero nunca anula el registro. Se anula el registro cuando una Task completa normalmente, pero no cuando se cancela. Mi conjetura es que la lógica errónea es que si la tarea se canceló, no hay necesidad de anular el registro de la cancelación, ya que tuvo que ser esa cancelación la que provocó la cancelación de la tarea.

La referencia conttask está allí, porque cont es en realidad ContinuationTaskFromResultTask (una clase que se deriva de Task ). Esta clase tiene un campo que contiene la tarea antecedente, que se anula cuando la continuación se ejecuta con éxito, pero no cuando se cancela.

como una adición … En este caso, el Finalizer se llama:

 WeakReference weakTask = null; using (var cts = new CancellationTokenSource()) { weakTask = Start(cts); } GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); Console.WriteLine(weakTask.IsAlive); // prints false