【问题标题】:Memory leak around timer delegate计时器委托周围的内存泄漏
【发布时间】:2012-11-03 01:08:37
【问题描述】:

有时用户想要安排大量计时器,但不想管理对这些计时器的引用。
如果用户没有引用定时器,定时器可能会在执行之前被 GC 收集。
我创建了 Timers 类作为新创建的计时器的占位符:

static class Timers
{
    private static readonly ILog _logger = LogManager.GetLogger(typeof(Timers));

    private static readonly ConcurrentDictionary<Object, Timer> _timers = new ConcurrentDictionary<Object, Timer>();

    /// <summary>
    /// Use this class in case you want someone to hold a reference to the timer.
    /// Timer without someone referencing it will be collected by the GC even before execution.
    /// </summary>
    /// <param name="dueTime"></param>
    /// <param name="action"></param>
    internal static void ScheduleOnce(TimeSpan dueTime, Action action)
    {
        if (dueTime <= TimeSpan.Zero)
        {
            throw new ArgumentOutOfRangeException("dueTime", dueTime, "DueTime can only be greater than zero.");
        }
        Object obj = new Object();

        Timer timer = new Timer(state =>
        {
            try
            {
                action();
            }
            catch (Exception ex)
            {
                _logger.ErrorFormat("Exception while executing timer. ex: {0}", ex);
            }
            finally
            {
                Timer removedTimer;
                if (!_timers.TryRemove(obj, out removedTimer))
                {
                    _logger.Error("Failed to remove timer from timers");
                }
                else
                {
                    removedTimer.Dispose();
                }
            }
        });
        if (!_timers.TryAdd(obj, timer))
        {
            _logger.Error("Failed to add timer to timers");
        }
        timer.Change(dueTime, TimeSpan.FromMilliseconds(-1));
    }
}

如果我不处置已删除的计时器,则会导致内存泄漏。
在计时器从_timers 集合中删除后,似乎有人持有对计时器委托的引用。

问题是,如果我不处置计时器,为什么会出现内存泄漏?

【问题讨论】:

  • 也许我不明白你在问什么,因为听起来你想知道为什么当你虐待它时会出现行为不端......
  • 我了解文档中说组件应该被处理掉。我仍然很好奇是什么阻止了 GC 在不调用 dispose 方法的情况下收集计时器和给定的委托。

标签: c# .net multithreading timer


【解决方案1】:

Timer 由计时器本身创建的GCHandle 保持活动状态。这可以使用 .net 内存分析器进行测试。反过来,Timer 将使代理保持活动状态,然后让其他人保持活动状态。

GCHandle 是一种特殊的对象,可用于“欺骗”垃圾收集器以使无法访问的对象保持活动状态。

您实际上可以在没有探查器的情况下使用以下方法进行测试:

var a = new ClassA();
var timer = new Timer(a.Exec);

var refA = new WeakReference(a);
var refTimer = new WeakReference(timer);

a = null;
timer = null;

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

Console.WriteLine(refA.IsAlive);
Console.WriteLine(refTimer.IsAlive);

【讨论】:

  • 这很有趣。我想知道为什么 System.Threading.Timer 文档说:“只要您使用 Timer,就必须保留对它的引用。与任何托管对象一样,当没有对 Timer 的引用时,它会受到垃圾回收的影响. Timer 仍然处于活动状态的事实并不能阻止它被收集。”我还注意到,如果没有保留对计时器的引用,则计时器可能不会被执行。
  • 看起来这就是开发人员想要的工作方式。如果我在 .net 4.0 或更高版本中运行我的测试程序,似乎对象确实被收集了。
【解决方案2】:

TimersComponents。因此,您必须在完成后致电Dispose

来自the documentation

组件应通过调用其Dispose 方法显式释放资源,而无需通过隐式调用Finalize 方法等待自动内存管理。当 Container 被释放时,Container 内的所有组件也会被释放。

“当Container 被释放时,Container 中的所有组件也被释放。”可以在 Form 的 Dispose 方法调用时看到:

if (disposing && (components != null))
{
    components.Dispose();
}

因此,除非将计时器添加到组件中,否则不要期望计时器与表单一起处置。

更新您的评论:
计时器具有指向非托管代码(操作系统的计时器 API)的指针,因此在不再需要这些代码之前它无法释放。除非首先调用 dispose 或程序正在退出,否则终结器不会在对象上运行。这是因为这些当前对非托管代码的引用。

据我了解,dispose 模型是为了加快程序关闭速度(因为运行时可以在停机期间收集垃圾),同时仍允许执行非托管代码。如果您执行大量 ddl 导入,您将开始了解系统为什么会这样工作。

还应注意,文档表明您可能无法从对象的终结器访问托管对象。 StreamWriter 就是一个例子。我个人认为这是一个任意规则,但它确实存在,因此需要 dispose 系统。

无论哪种方式,如果您使用了实现 iDisposable 接口的东西,您应该始终在使用完后将其丢弃。这样你会得到更好(更一致)的结果。

【讨论】:

  • 对。但是计时器将由 GC 收集,然后调用将释放委托和其余资源的终结器。我的问题是为什么在这种情况下需要 Dispose 方法。
  • @OronNadiv 计时器具有指向非托管代码(操作系统的计时器 API)的指针,因此在不再需要这些代码之前无法释放。除非首先调用 dispose 或由于这些当前对非托管代码的引用而导致程序退出,否则终结器将不会在对象上运行。
  • 看来他使用的是System.Threading.Timer,而不是Component
  • @JimMischel 是的,看来我错过了。
猜你喜欢
  • 1970-01-01
  • 2010-12-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-20
  • 2012-01-30
相关资源
最近更新 更多