28

当您使用将在程序的整个生命周期中运行的 aTimer或 aThread时,您是否需要保留对它们的引用以防止它们被垃圾收集?

请抛开以下程序timer在类中可以作为静态变量的事实,这只是一个展示问题的玩具示例。

public class Program
{
    static void Main(string[] args)
    {
        CreateTimer();

        Console.ReadLine();
    }

    private static void CreateTimer()
    {
        var program = new Program();

        var timer = new Timer();
        timer.Elapsed += program.TimerElapsed;
        timer.Interval = 30000;
        timer.AutoReset = false;
        timer.Enabled = true;
    }

    private void TimerElapsed(object sender, ElapsedEventArgs e)
    {
        var timerCast = (Timer)sender;

        Console.WriteLine("Timer fired at in thread {0}", GetCurrentThreadId());

        timerCast.Enabled = true;
    }

    ~Program()
    {
        Console.WriteLine("Program Finalized");
    }

    [DllImport("kernel32.dll")]
    static extern uint GetCurrentThreadId();
}

可以在上面的示例中收集计时器吗?我运行了一段时间,我从来没有收到异常,也没有消息说~Program()被调用。

更新:我从这个问题中发现(感谢sethcran)线程被CLR跟踪,但我仍然想要一个关于定时器的答案。

4

3 回答 3

56

这只是 System.Threading.Timer 类的问题,如果您没有在某处存储对它的引用。它有几个构造函数重载,接受状态对象的重载很重要。CLR 关注该状态对象。只要在某处引用它,CLR 就会将计时器保留在其计时器队列中,并且计时器对象不会被垃圾回收。大多数程序员不会使用那个状态对象,MSDN 文章当然没有解释它的作用。

System.Timers.Timer 是 System.Threading.Timer 类的包装器,使其更易于使用。特别是,只要启用了计时器,它将使用该状态对象并保持对它的引用。

请注意,在您的情况下,计时器的 Enabled 属性在进入您的 Elapsed 事件处理程序时为 false,因为您有 AutoReset = false。因此,一旦计时器进入您的事件处理程序,它就有资格被收集。但是您可以通过引用sender参数来避免麻烦,需要将 Enabled 设置回 true。这使得抖动报告参考,所以你没有问题。

请注意 Elapsed 事件处理程序。该方法中抛出的任何异常都将在没有诊断的情况下被吞下。这也意味着您不会将 Enabled 设置回 true。你必须使用 try/catch 来做一些合理的事情。如果你不打算故意结束你的程序,至少你需要让你的主程序知道某些东西不再工作了。将 Enabled = true 放在 finally 子句中可以避免收集计时器垃圾,但有可能让您的程序一遍又一遍地抛出异常。

于 2013-08-08T22:19:12.847 回答
4

我们来做一个实验:

private static void UnderTest() {
  // Timer is a local varibale; its callback is local as well
  System.Threading.Timer timer = new System.Threading.Timer(
    (s) => { MessageBox.Show("Timer!"); }, 
     null, 
     1000, 
     1000);
}

...

// Let's perform Garbage Colelction manually: 
// we don't want any surprises 
// (e.g. system starting collection in the middle of UnderTest() execution)
GC.Collect(2, GCCollectionMode.Forced);

UnderTest();

// To delay garbage collection
// Thread.Sleep(1500);

// To perform Garbage Collection
// GC.Collect(2, GCCollectionMode.Forced);

至今

  • 如果我们继续评论两者Thread.Sleep(1500);GC.Collect(2, GCCollectionMode.Forced);我们会看到消息框出现:计时器正在工作
  • 如果我们取消注释 GC.Collect(2, GCCollectionMode.Forced);,我们将什么也看不到:计时器启动然后它被收集
  • 如果我们取消注释两者Thread.Sleep(1500);GC.Collect(2, GCCollectionMode.Forced);我们将看到一个消息框:计时器启动,关闭单个消息框,然后收集计时器

所以System.Threading.Timers 被收集为任何其他对象实例。

于 2019-01-10T07:33:24.267 回答
3

将此代码添加到程序并运行它。你会看到计时器没有被收集。

    private void DoStuff()
    {
        CreateTimer();
        Console.WriteLine("Timer started");
        int count = 0;
        for (int x = 0; x < 1000000; ++x)
        {
            string s = new string("just trying to exercise the garbage collector".Reverse().ToArray());
            count += s.Length;
        }
        Console.WriteLine(count);
        Console.Write("Press Enter when done:");
        Console.ReadLine();
    }

    private void Ticktock(object s, System.Timers.ElapsedEventArgs e)
    {
        Console.WriteLine("Ticktock");
    }

    private void CreateTimer()
    {
        System.Timers.Timer t = new System.Timers.Timer(); // Timer(Ticktock, null, 1000, 1000);
        t.Elapsed += Ticktock;
        t.Interval = 1000;
        t.AutoReset = true;
        t.Enabled = true;
    }

因此,您的问题的答案似乎是计时器不符合收集条件,如果您不维护对它的引用,则不会被收集。

有趣的是,如果您使用 运行相同的测试System.Threading.Timer,您会发现计时器已被收集。

于 2013-08-08T22:19:05.877 回答