【问题标题】:Lambdas within Extension methods: Possible memory leak?扩展方法中的 Lambda:可能的内存泄漏?
【发布时间】:2010-04-06 10:10:25
【问题描述】:

我刚刚使用扩展方法回答了quite simple question。但写下来后,我记得你不能从事件处理程序中取消订阅 lambda。

到目前为止没有什么大问题。但是这一切在扩展方法中是如何表现的呢?

下面是我的代码再次被剪断。那么,如果您多次调用此扩展方法,是否会导致无数计时器在内存中徘徊?

我会说不,因为计时器的范围被限制在这个函数中。所以离开它之后,没有其他人可以引用这个对象。我只是有点不确定,因为我们在静态类的静态函数中。

public static class LabelExtensions
{
    public static Label BlinkText(this Label label, int duration)
    {
        System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();

        timer.Interval = duration;
        timer.Tick += (sender, e) =>
            {
                timer.Stop();
                label.Font = new Font(label.Font, label.Font.Style ^ FontStyle.Bold);
            };

        label.Font = new Font(label.Font, label.Font.Style | FontStyle.Bold);
        timer.Start();

        return label;
    }
}

更新

只是为了澄清我使用了System.Windows.Forms.Timer。因此,从您的回答看来,尤其是使用这个计时器类是正确的选择,因为在这种情况下,它会按照我所期望的方式执行任何操作。如果您在这种情况下尝试另一个计时器类,您可能会遇到Matthew found out 的麻烦。 如果我的对象是否还活着,我也可以使用 WeakReferences found a way

更新 2

经过一点睡眠和更多思考后,我还对我的测试仪(answer below)进行了另一项更改,我只是在最后一行之后添加了一个GC.Collect(),并将持续时间设置为 10000。在启动 BlinkText() 之后有几次我一直按我的 button2 来获取当前状态并强制进行垃圾收集。而且看起来所有的计时器都将在调用Stop() 方法之后被销毁。因此,当我的 BlinkText 已经离开并且计时器正在运行时,垃圾收集也不会导致任何问题。

因此,在您的所有良好响应和更多测试之后,我可以很高兴地说,它只是做了它应该做的事情,而不会将计时器留在内存中,也不会在计时器完成工作之前将其丢弃。

【问题讨论】:

  • 你的代码会因为非法的跨线程调用而失败。您的计时器(特别是 Winforms 计时器)需要有一个同步对象。
  • @leppie:错了,这个 Timer 正在使用 messagepump。试试看。
  • @Henk Holterman:谢谢,看来我把它和System.Timers.Timer 混淆了。

标签: c# lambda extension-methods


【解决方案1】:

你的假设是正确的。 timer 将在方法被调用时分配,并在最后有资格进行垃圾回收。

lamba 事件处理程序(假设它没有在其他地方引用)也有资格进行垃圾回收。

这是一个静态方法和/或扩展方法这一事实不会改变对象可达性的基本规则。

【讨论】:

  • 这是否意味着如果您在持续时间之前调用 GC.Collect 可能会导致闪烁失败?
  • @Alxandr:不,活动的计时器仍然可以访问,GC 不会收集它。
【解决方案2】:

您的代码是正确的,不会泄漏内存或资源,但只会因为您在事件处理程序中停止了 Timer。如果您注释掉 //timer.Stop();,它会一直闪烁,即使您稍后执行 GC.Collect(); 也是如此。

Timer 分配一个 Window 来监听 WM_ 消息,并以某种方式锚定于此。当计时器停止 (Enabled = false) 时,它会释放该窗口。
staticextension method 上下文不起作用。

【讨论】:

  • 仅仅因为你 .Stop() 定时器它不会神奇地被处理掉。系统仍然持有对该对象的引用,即使它没有做任何事情。
  • 我收回我说的话。仔细观察System.Windows.Forms.Timer,它确实能够在您调用 stop 时释放 GCHandle。
【解决方案3】:

内存泄漏不是您的问题。垃圾收集器可以释放计时器对象。但是虽然计时器没有停止,但似乎有对计时器的引用,因此不会过早地处理它。要检查这一点,请从 Timer 派生一个 MyTimer 类,重写 Dispose(bool) 并设置断点。在 BlinkText 调用 GC.Collect 和 GC.WaitForPendingFinalizers 之后。第二次调用后,第一个定时器实例被释放。

【讨论】:

  • 我无法验证(在调用 BlinkText 后直接调用 GC.Collect)。
【解决方案4】:

我刚刚按照@cornerback84 所说的内容做了一个有趣的测试。我创建了一个带有标签和两个按钮的表单。一个按钮绑定了您的扩展方法,另一个绑定了强制GC.Collect()。然后我在您的事件循环中删除了timer.Stop()。多次点击开始按钮真的很有趣。眨眼的间隔非常混乱。

这里看起来确实存在内存/资源泄漏……但话说回来,Timer 是一个永远不会被释放的一次性对象。

编辑:...

让乐趣开始吧...这也可能取决于您使用的Timer

  • System.Windows.Forms.Timer => 这个 不会收集,但会正常工作 系统消息泵很好。如果您致电.Stop(),此对象可能有资格收集。

  • System.Timers.Timer => 这不是 使用消息泵。你也多 调用消息泵。这不会 收藏。如果您致电.Stop(),此对象可能有资格收集。

  • System.Threading.Timer => 这需要调用消息泵。但这也会在GC.Collect() 上停止(这个版本似乎没有泄漏)

【讨论】:

  • 不处理 IDisposable 不是内存泄漏。只是效率低下。
  • @Henk:这很可能导致内存和资源泄漏。
  • 马修,一个常见的误解。需要它的 IDisposables 有一个析构函数(终结器)。清理只是延迟了。
  • @Henk:如果系统中的某些东西持有对对象的引用,则永远不会调用终结器。还有一些实现 IDisposable 的对象没有终结器(感谢这些类的作者;)。您的终结器也很有可能永远不会被调用。 GC 100% 可以调用它。如果它不能足够快地完成工作,GC 可能会终止调用。
  • “GC 可以终止呼叫” - 你确定吗?其余的见stackoverflow.com/questions/2101524/…
【解决方案5】:

似乎 Timer 会一直存活到程序退出。我尝试处理 Label 并将其引用设置为 null,但直到我退出程序才收集 Timer。

【讨论】:

  • 编译发布并强制收集。
  • 我这样做了,但它没有被收集。
  • 是的,我只是想知道它是否会被清理。我的猜测是 Timer 是一次性的。某些东西必须保留对计时器对象的引用,然后它又可以是处理事件循环的线程。很有可能因为事件引用而迫使对象保持活动状态。所以是的,这里有很多泄漏。
  • 很高兴看到人们仍然在不留票的情况下投票。
【解决方案6】:

在阅读了你所有的答案后,我找到了一种更好的测试方法。

我通过以下方式更新了我的扩展类:

public static class LabelExtensions
{
    public static List<WeakReference> _References = new List<WeakReference>();

    public static Label BlinkText(this Label label, int duration)
    {
        Timer timer = new Timer();

        _References.Add(new WeakReference(timer));

        timer.Interval = duration;
        timer.Tick += (sender, e) =>
        {
            timer.Stop();
            label.Font = new Font(label.Font, label.Font.Style ^ FontStyle.Bold);
        };

        label.Font = new Font(label.Font, label.Font.Style | FontStyle.Bold);
        timer.Start();

        return label;
    }
}

我还创建了第二个按钮、第二个标签并将以下代码添加到点击事件中:

    private void button2_Click(object sender, EventArgs e)
    {
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < LabelExtensions._References.Count; i++)
        {
            var wr = LabelExtensions._References[i];
            sb.AppendLine(i + " alive: " + wr.IsAlive);
        }

        label2.Text = sb.ToString();
    }

现在我只是按了第一个按钮几次,让第一个标签闪烁。然后我按下我的第二个按钮并得到一个列表,我可以在其中查看我的计时器是否还活着。在第一次点击时,他们都得到了一个真实的。但是后来我再次点击了我的第一个按钮,当我更新我的状态消息时,我看到第一个项目在 IsAlive 中已经是错误的。所以我可以肯定地说这个函数不会导致任何内存问题。

【讨论】:

    【解决方案7】:

    你应该通过调用它的 Dispose 方法来显式地释放 Timer,或者在使用它之后通过调用 Stop 来隐式地释放它,这样你的实现是正确的并且永远不会导致内存泄漏。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-03-10
      • 1970-01-01
      • 1970-01-01
      • 2018-05-30
      • 2019-07-16
      • 2011-01-03
      • 1970-01-01
      相关资源
      最近更新 更多