任务未被垃圾回收

10
在下面的程序中,我希望任务得到垃圾回收,但它没有。我使用了一个内存分析器,显示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);
}

调试/发布模式? - H H
可能是了解.NET中的垃圾回收的重复问题。 - Hans Passant
当GC完成时,任务肯定还没有完成。 - Hans Passant
汉斯,不是这样的。我正在使用弱引用。还有一个WaitOne()。任务状态在尝试GC之前被打印并且已经失败了。看一下代码 :) - Eli Arbel
我刚刚检查了一下...任务终结器没有被调用...这很有趣。 - Vladimir Gondarev
2个回答

5

简短回答:我认为这是内存泄漏(或两个,见下文),你应该报告它。

长篇回答:

Task没有被GC的原因是因为它从CTS中可达,如下所示:ctsconttask。我认为在您的情况下,这两个引用都不应存在。

ctscont引用之所以存在,是因为cont正确地使用令牌注册了取消,但它从未注销。当Task正常完成时,它会注销,但当它被取消时不会。我的猜测是错误的逻辑是,如果任务被取消,就没有必要从取消中注销,因为必须是取消导致任务被取消。

这里有一个 conttask 的引用,因为 cont 实际上是从 Task 派生的 ContinuationTaskFromResultTask 类。这个类有一个字段来保存前置任务,当继续执行成功时会将其设置为空,但取消时不会。


叹气 :) 我本来希望有更好的消息。我已经写信给 PFX 团队的一位成员,希望他们能确认这个 bug。谢谢! - Eli Arbel

-1

作为一个补充...

在这种情况下,Finalizer 被调用:

WeakReference weakTask = null;
using (var cts = new CancellationTokenSource())
{
  weakTask = Start(cts);
}

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

Console.WriteLine(weakTask.IsAlive); // prints false

有道理。但这应该是一条评论,而不是一个答案。 - Eli Arbel
顺便提一下,在Release版本中甚至不需要使用using;如果您删除GC.KeepAlive,则CTS将被收集,因此任务也会被收集。但这对我的情况没有帮助。 CTS存储在字段中很长时间。 - Eli Arbel
CTS被长期存储在一个字段中。为什么? - Vladimir Gondarev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接