zoukankan      html  css  js  c++  java
  • 【C#】定时器保活机制引起的内存泄露问题

    C# 中有三种定时器,System.Windows.Forms 中的定时器和 System.Timers.Timer 的工作方式是完全一样的,所以,这里我们仅讨论 System.Timers.TimerSystem.Threading.Timer

    1、定时器保活

    先来看一个例子:

    class Program
    {
        static void Main(string[] args)
        {
            Start();
    
            GC.Collect();
            Read();
        }
    
        static void Start()
        {
            Foo f = new Foo();
            System.Threading.Thread.Sleep(5_000);
        }
    }
    
    public class Foo
    {
        System.Timers.Timer _timer;
    
        public Foo()
        {
            _timer = new System.Timers.Timer(1000);
            _timer.Elapsed += timer_Elapsed;
            _timer.Start();
        }
    
        private void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            WriteLine("System.Timers.Timer Elapsed.");
        }
        
        ~Foo()
        {
            WriteLine("---------- End ----------");
        }
    }
    

    运行结果如下:

    System.Timers.Timer Elapsed.
    System.Timers.Timer Elapsed.
    System.Timers.Timer Elapsed.
    System.Timers.Timer Elapsed.
    System.Timers.Timer Elapsed.
    System.Timers.Timer Elapsed.
    System.Timers.Timer Elapsed.
    ...
    

    Start 方法结束后,Foo 实例已经失去了作用域,按理说应该被回收,但实际并没有(因为析构函数没有执行,所以肯定实例未被回收)。

    这就是定时器的 保活机制,因为定时器需要执行 timer_Elapsed 方法,而该方法属于 Foo 实例,所以 Foo 实例被保活了。

    但多数时候这并不是我们想要的结果,这种结果导致的结果就是 内存泄露,解决方案是:先将定时器 Dispose

    public class Foo : IDisposable
    {
        ...
        public void Dispose()
        {
            _timer.Dispose();
        }
    }
    

    一个很好的准则是:如果类中的任何字段所赋的对象实现了IDisposable 接口,那么该类也应当实现 IDisposable 接口。

    在这个例子中,不止 Dispose 方法,Stop 方法和设置 AutoReset = false,都能起到释放对象的目的。但是如果在 Stop 方法之后又调用了 Start 方法,那么对象依然会被保活,即便 Stop 之后进行强制垃圾回收,也无法回收对象。

    System.Timers.TimerSystem.Threading.Timer 的保活机制是类似的。

    保活机制是由于定时器引用了实例中的方法,那么,如果定时器不引用实例中的方法呢?

    2、不保活下 System.Timers.TimerSystem.Threading.Timer 的差异

    要消除定时器对实例方法的引用也很简单,将 timer_Elapsed 方法改成 静态 的就好了。(静态方法属于类而非实例。)

    改成静态方法后再次运行示例,结果如下:

    System.Timers.Timer Elapsed.
    System.Timers.Timer Elapsed.
    System.Timers.Timer Elapsed.
    System.Timers.Timer Elapsed.
    ---------- End ----------
    System.Timers.Timer Elapsed.
    System.Timers.Timer Elapsed.
    System.Timers.Timer Elapsed.
    ...
    

    Foo 实例是被销毁了(析构函数已运行,打印出了 End),但定时器还在执行,这是为什么呢?

    这是因为,.NET Framework 会确保 System.Timers.Timer 的存活,即便其所属实例已经被销毁回收。

    如果改成 System.Threading.Timer,又会如何?

    class Program
    {
        static void Main(string[] args)
        {
            Start();
    
            GC.Collect();
            Read();
        }
    
        static void Start()
        {
            Foo2 f2 = new Foo2();
            System.Threading.Thread.Sleep(5_000);
        }
    }
    
    public class Foo2
    {
        System.Threading.Timer _timer;
    
        public Foo2()
        {
            _timer = new System.Threading.Timer(timerTick, null, 0, 1000);
        }
    
        static void timerTick(object state)
        {
            WriteLine("System.Threading.Timer Elapsed.");
        }
    
        ~Foo2()
        {
            WriteLine("---------- End ----------");
        }
    }
    

    注意,这里的 timerTick 方法是静态的。运行结果如下:

    System.Threading.Timer Elapsed.
    System.Threading.Timer Elapsed.
    System.Threading.Timer Elapsed.
    System.Threading.Timer Elapsed.
    System.Threading.Timer Elapsed.
    ---------- End ----------
    

    可见,随着 Foo2 实例销毁,_timer 也自动停止并销毁了。

    这是因为,.NET Framework 不会保存激活 System.Threading.Timer 的引用,而是直接引用回调委托。

  • 相关阅读:
    一些前台技巧
    javascript中的面向对象
    vs.net 常用快捷键
    js和C#中的进制转换方法
    MFC中CDC相关图形,文本的一些使用方法(转)
    poj1505
    poj1401
    poj2533
    poj1504
    poj1384
  • 原文地址:https://www.cnblogs.com/gl1573/p/12267800.html
Copyright © 2011-2022 走看看