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

作者:丹枫无迹 时间:2022-09-19 00:31:33 

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 的引用,而是直接引用回调委托。

来源:https://www.cnblogs.com/gl1573/p/12267800.html

标签:C#,定时器保活机制
0
投稿

猜你喜欢

  • android手机端与PC端使用adb forword通信

    2022-09-15 14:17:35
  • android自由改变Dialog窗口位置的方法

    2021-09-13 04:19:04
  • Android的OkHttp包中的HTTP拦截器Interceptor用法示例

    2022-01-31 06:31:25
  • Unity实现俄罗斯方块(一)

    2021-06-07 15:30:24
  • Android开机画面的具体修改方法

    2023-05-13 20:52:22
  • Java中ArrayList类的用法与源码完全解析

    2023-07-22 02:32:49
  • Java定时器Timer简述

    2023-07-20 19:17:16
  • SpringBoot四种读取properties文件的方式(小结)

    2023-10-10 03:25:14
  • Unity Shader实现径向模糊效果

    2021-09-06 01:02:47
  • c#唯一值渲染实例代码

    2023-09-06 06:54:16
  • Android RadioGroup多行显示效果 解决单选问题

    2023-05-28 18:23:55
  • java两种单例模式用法分析

    2023-10-06 03:51:34
  • 详解Java内存溢出的几种情况

    2023-11-13 19:46:18
  • java8 如何实现分组计算数量和计算总数

    2022-05-05 01:17:32
  • Android学习笔记之ActionBar Item用法分析

    2022-03-12 00:18:59
  • response文件流输出文件名中文不显示的解决

    2023-02-06 19:41:02
  • Android编程ProgressBar自定义样式之动画模式实现方法

    2022-02-10 12:54:29
  • Android ListView弹性效果的实现方法

    2023-08-07 19:06:40
  • MyBatis配置的应用与对比jdbc的优势

    2023-08-27 07:03:47
  • SpringBoot 自定义+动态切换数据源教程

    2021-10-13 21:30:39
  • asp之家 软件编程 m.aspxhome.com