C# 定时器保活机制引起的内存泄露问题解决

所属分类: 软件编程 / C#教程 阅读数: 65
收藏 0 赞 0 分享

C# 中有三种定时器,System.Windows.Forms 中的定时器和 System.Timers.Timer 的工作方式是完全一样的,所以,这里我们仅讨论 System.Timers.Timer 和 System.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.Timer System.Threading.Timer 的保活机制是类似的。

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

2、不保活下 System.Timers.Timer 和 System.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 的引用,而是直接引用回调委托。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

更多精彩内容其他人还在看

WPF仿三星手机充电界面实现代码

这篇文章主要为大家详细介绍了WPF仿三星手机充电界面实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
收藏 0 赞 0 分享

浅谈C#各种数组直接的数据复制/转换

下面小编就为大家带来一篇浅谈C#各种数组直接的数据复制/转换。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
收藏 0 赞 0 分享

C#访问SQLServer增删改查代码实例

这篇文章主要为大家详细介绍了C#访问SQLServer增删改查代码实例,感兴趣的小伙伴们可以参考一下
收藏 0 赞 0 分享

C#根据身份证号码判断出生日期和性别

这篇文章主要为大家详细介绍了C#根据身份证号码判断出生日期和性别的方法,感兴趣的小伙伴们可以参考一下
收藏 0 赞 0 分享

C# 向Word中设置/更改文本方向的方法(两种)

在一般情况下word中输入的文字都是横向的,今天小编给大家带来两种方法来设置更改文本方向的方法,非常不错,对c# word 更改文本方向的知识感兴趣的朋友一起看看吧
收藏 0 赞 0 分享

让C# Excel导入导出 支持不同版本Office

让C# Excel导入导出,支持不同版本的Office,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
收藏 0 赞 0 分享

C#设置自定义文件图标实现双击启动(修改注册表)

这篇文章介绍的是利用C#设置自定义文件图标,然后实现双击启动的功能,文章给出了示例代码,介绍的很详细,有需要的可以参考借鉴。
收藏 0 赞 0 分享

C#两个相同属性的类赋值方法

这篇文章主要介绍了C#两个相同属性的类赋值方法的相关资料,需要的朋友可以参考下
收藏 0 赞 0 分享

C#中ListView控件实现窗体代码

这篇文章主要介绍了C#中ListView控件实现窗体的核心代码,非常不错,具有参考借鉴价值,对c#listview相关知识感兴趣的朋友一起学习吧
收藏 0 赞 0 分享

浅谈C# 序列化与反序列化几种格式的转换

下面小编就为大家带来一篇浅谈C# 序列化与反序列化几种格式的转换。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
收藏 0 赞 0 分享
查看更多