欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2023-11-22 11:42:46
c# 中有三种定时器,system.windows.forms 中的定时器和 system.timers.timer 的工作方式是完全一样的,所以,这里我们仅讨论 system.timers.time...

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

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