任务不是垃圾收集
在下面的程序中,我希望任务能够得到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团队已确认这似乎是泄漏。 作为解决方法,我们在使用取消令牌时已停止使用任何延续条件。 相反,我们总是执行continuation,检查内部条件,如果不满足则抛出OperationCanceledException
。 这保留了延续的语义。 以下扩展方法封装了这个:
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); }
简短的回答:我认为这是一个内存泄漏(或两个,见下文),你应该报告它 。
答案很长:
Task
未被GC的原因是因为它可以从CTS到达: cts
→ cont
→ task
。 我认为这两个引用都不应该存在于您的情况中。
cts
→ cont
引用是因为cont
使用令牌正确注册取消,但它从不注销。 当Task
正常完成时,它会取消注册,但在取消时则不会注销。 我的猜测是错误的逻辑是,如果任务被取消,则无需取消注册,因为必须是取消导致任务被取消。
cont
→ task
引用就在那里,因为cont
实际上是ContinuationTaskFromResultTask
(一个派生自Task
的类)。 这个类有一个包含先行任务的字段,当成功执行延续时,该字段被清空,但是当它被取消时则不被清除。
作为补充…在这种情况下,正在调用终结器:
WeakReference weakTask = null; using (var cts = new CancellationTokenSource()) { weakTask = Start(cts); } GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); Console.WriteLine(weakTask.IsAlive); // prints false