计时器委托周围的内存泄漏

时间:2012-11-03 01:08:37

标签: c# .net multithreading timer

有时,用户希望安排大量计时器,并且不希望管理对这些计时器的引用。
如果用户没有引用计时器,则可以在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集合中删除计时器后,正在控制对Timer的委托的引用。

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

2 个答案:

答案 0 :(得分:9)

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);

答案 1 :(得分:4)

TimersComponents。因此,当您完成它们时,您必须致电Dispose

来自the documentation

  

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

部分“处置Container时,Container内的所有组件也会被处理掉。”当它调用时,可以在Form的Dispose方法中看到:

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

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

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

据我所知,dispose模型假设加速程序关闭(因为运行时可以在停机期间收集垃圾),同时仍允许执行非托管代码。如果您执行大量的ddl导入,您将开始了解系统的工作原理。

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

无论哪种方式,如果您使用实现iDisposable接口的东西,您应该在完成后将其丢弃。通过这种方式,您将获得更好(更一致)的结果。