为什么定时器让我的对象保持活着?

时间:2013-01-08 13:33:38

标签: c# events timer

前言:我知道如何解决问题。我想知道为什么它出现了。请从上到下阅读问题。

正如我们所有人(应该)都知道的那样,添加事件处理程序会导致C#中的内存泄漏。见Why and How to avoid Event Handler memory leaks?

另一方面,对象通常具有相似或相关的生命周期,并且不需要取消注册事件处理程序。考虑这个例子:

using System;

public class A
{
    private readonly B b;

    public A(B b)
    {
        this.b = b;
        b.BEvent += b_BEvent;
    }

    private void b_BEvent(object sender, EventArgs e)
    {
        // NoOp
    }

    public event EventHandler AEvent;
}

public class B
{
    private readonly A a;

    public B()
    {
        a = new A(this);
        a.AEvent += a_AEvent;
    }

    private void a_AEvent(object sender, EventArgs e)
    {
        // NoOp
    }

    public event EventHandler BEvent;
}

internal class Program
{
    private static void Main(string[] args)
    {
        B b = new B();

        WeakReference weakReference = new WeakReference(b);
        b = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();

        bool stillAlive = weakReference.IsAlive; // == false
    }
}

AB通过事件隐式引用彼此,但GC可以删除它们(因为它不使用引用计数,而是标记和扫描)。

但现在考虑这个类似的例子:

using System;
using System.Timers;

public class C
{
    private readonly Timer timer;

    public C()
    {
        timer = new Timer(1000);
        timer.Elapsed += timer_Elapsed;
        timer.Start(); // (*)
    }

    private void timer_Elapsed(object sender, ElapsedEventArgs e)
    {
        // NoOp
    }
}

internal class Program
{
    private static void Main(string[] args)
    {
        C c = new C();

        WeakReference weakReference = new WeakReference(c);
        c = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
        bool stillAlive = weakReference.IsAlive; // == true !
    }
}

为什么GC不能删除C对象?为什么Timer保持对象存活?定时器机制的一些“隐藏”参考(例如静态参考)是否使计时器保持活动状态?

(*)注意:如果仅创建但未启动计时器,则不会发生此问题。如果它已启动并稍后停止,但事件处理程序未取消注册,则问题仍然存在。

6 个答案:

答案 0 :(得分:5)

定时器逻辑依赖于OS功能。它实际上是触发事件的操作系统。操作系统依次使用CPU interrupts来实现它。

OS API(又名Win32)不保留对任何类型的任何对象的引用。它保存了在发生计时器事件时必须调用的函数的内存地址。 .NET GC无法跟踪此类“引用”。因此,可以收集计时器对象而无需取消订阅低级事件。这是一个问题,因为OS无论如何都会尝试调用它,并且会因一些奇怪的内存访问异常而崩溃。这就是为什么.NET Framework在静态引用的对象中保存所有这样的计时器对象,并且只有在取消订阅时才从该集合中删除它们。

如果您使用SOS.dll查看对象的根目录,您将获得下一张图片:

!GCRoot 022d23fc
HandleTable:
    001813fc (pinned handle)
    -> 032d1010 System.Object[]
    -> 022d2528 System.Threading.TimerQueue
    -> 022d249c System.Threading.TimerQueueTimer
    -> 022d2440 System.Threading.TimerCallback
    -> 022d2408 System.Timers.Timer
    -> 022d2460 System.Timers.ElapsedEventHandler
    -> 022d23fc TimerTest.C

然后,如果你看看像dotPeek这样的System.Threading.TimerQueue类,你会看到它被实现为一个单例并且它包含一组计时器。

这就是它的工作原理。不幸的是,MSDN文档并不是很清楚。他们只是假设如果它实现了IDisposable,那么你应该毫无疑问地处理它。

答案 1 :(得分:3)

  

定时器机制的某些“隐藏”引用(例如静态引用)是否使定时器保持活动状态?

是。它是在CLR中构建的,当您使用参考源或反编译器(Timer类中的私有“cookie”字段)时,您可以看到它的痕迹。它作为第二个参数传递给实际实现计时器的System.Threading.Timer构造函数,即“状态”对象。

CLR保留已启用的系统计时器列表,并添加对状态对象的引用,以确保它不会被垃圾回收。这反过来确保Timer对象不会被垃圾收集,只要它在列表中。

因此,收集System.Timers.Timer垃圾需要您调用其Stop()方法或将其Enabled属性设置为false,同样的事情。这导致CLR从活动计时器列表中删除系统计时器。这也删除了对 state 对象的引用。然后使计时器对象有资格进行收集。

显然,这是理想的行为,您通常不希望计时器消失并在其处于活动状态时停止计时。使用System.Threading.Timer时会发生 ,如果您没有明确地或使用状态继续引用它,它会停止调用它的回调对象

答案 2 :(得分:2)

我认为这与Timer的实现方式有关。当你调用Timer.Start()时,它设置Timer.Enabled = true。看看Timer.Enabled的实现:

public bool Enabled
{
    [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
    get
    {
        return this.enabled;
    }
    set
    {
        if (base.DesignMode)
        {
            this.delayedEnable = value;
            this.enabled = value;
        }
        else if (this.initializing)
        {
            this.delayedEnable = value;
        }
        else if (this.enabled != value)
        {
            if (!value)
            {
                if (this.timer != null)
                {
                    this.cookie = null;
                    this.timer.Dispose();
                    this.timer = null;
                }
                this.enabled = value;
            }
            else
            {
                this.enabled = value;
                if (this.timer == null)
                {
                    if (this.disposed)
                    {
                        throw new ObjectDisposedException(base.GetType().Name);
                    }
                    int dueTime = (int) Math.Ceiling(this.interval);
                    this.cookie = new object();
                    this.timer = new Timer(this.callback, this.cookie, dueTime, this.autoReset ? dueTime : 0xffffffff);
                }
                else
                {
                    this.UpdateTimer();
                }
            }
        }
    }
}

它看起来像是一个新的计时器,传递给它一个cookie对象(很奇怪!)。在该调用路径之后会导致其他一些涉及创建TimerHolder和TimerQueueTimer的复杂代码。我希望在某些时候创建一个在Timer外部保存的引用,直到你调用Timer.Stop()或Timer.Enabled = false为止。

这不是一个明确的答案,因为我发布的代码都没有创建这样的引用;但是它的内容很复杂,导致我怀疑这样的事情正在发生。

如果你有反射器(或类似)看看,你会明白我的意思。 :)

答案 3 :(得分:1)

因为Timer仍然有效。 (Timer.Elapsed)不会删除事件处理程序。

如果要妥善处置,请实施IDisposable界面,在Dispose方法中删除事件处理程序,并使用using块或手动调用Dispose。问题不会发生。

实施例

 public class C : IDisposable  
 {
    ...

    void Dispose()
    {
      timer.Elapsed -= timer_elapsed;
    }
 }

然后

 C c = new C();

 WeakReference weakReference = new WeakReference(c);
 c.Dispose();
 c = null;

答案 4 :(得分:0)

我认为问题来自这条线;

c = null;

通常,大多数开发人员认为使对象等于null会导致对象被垃圾收集器删除。但这种情况并非如此;实际上只删除了对内存位置(创建c对象)的引用;如果对相关内存位置有任何其他引用,则不会将对象标记为删除。在这种情况下,由于Timer引用了相关的内存位置,因此垃圾收集器不会删除对象。

答案 5 :(得分:0)

让我们先谈谈Threading.Timer。在内部,计时器将使用回调和状态传递给Timer ctor来构造TimerQueueTimer对象(比如新的Threading.Timer(回调,状态,xxx,xxx).TimerQueueTimer将被添加到静态列表中。

如果回调方法和状态没有“this”信息(比如使用静态方法进行回调而null为状态),那么Timer对象可以在没有引用时进行GCed。 另一方面,如果使用成员方法进行回调,则包含“this”的委托将存储在上述静态列表中。所以Timer对象不能被GC,因为仍然引用了“C”(在你的例子中)对象。

现在让我们回到System.Timers.Timer,它内部包装了Threading.Timer。请注意,当前者构造后者时,会使用System.Timers.Timer成员方法,因此无法对System.Timers.Timer对象进行GC。