1. 程式人生 > 程式設計 >C# 定時器保活機制引起的記憶體洩露問題解決

C# 定時器保活機制引起的記憶體洩露問題解決

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

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。