次のプログラムでは、タスクが GC されることを期待しますが、そうではありません。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);
}