一. 問題來源
在我上家公司里,做停車軟件客戶端的時候,崗亭客戶端需要每隔一段時間,將本地時間和服務所在的電腦上的時間,和中央服務器上的本地時間進行同步。但是在實際運用的時候,打開客戶端除了開啟計時器(System.Threading.Timer)的時候會同步一次以外,之后就再也不會同步。
二. 關於 System.Threading.Timer
System.Threading.Timer 是一個比較特殊的對象,在程序還沒有執行到離開 System.Threading.Timer 的作用域的時候。如果發生一次 GC 的回收,那么在 Release 編譯的模式下,這個計時器會直接被當做垃圾而被 CLR 回收,造成無法正常進行定時操作。
當然在 Debug 編譯模式下,並不會發生這個問題。因為在 Debug 模式下,編譯器會添加相關的方法特性(編譯器會為程序集設置 DebuggingModes 的 DisableOptimizations 標志)來阻止 CLR 垃圾回收器在離開作用域之前回收它,將所有根的生存周期延長至方法結束。
值得一提的是,只要有一個根在引用它,對於其他的對象,並不會造成在離開作用域之前就被回收。所以 System.Threading.Timer 需要我們在某些特殊情況下區別於其他對象進行對待。
三. 解決 System.Threading.Timer 被提前回收的方法
解決這個被提前回收的方法主要有一下幾種:
1. 如果是在命令窗口中編譯,使用 " /Debug+ " ,於此同時不要去啟用 " /optimize "。如果是在VS中那么直接在上面的工具欄中選擇 " Debug " 即可。當然這種方法是有缺點的,就是不能處理 Release 編譯的程序;
操作步驟:
step 1:測試代碼(已經在應用進行精簡刪除 僅僅保留命名空間 using System )
using System; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { System.Threading.Timer timer = new System.Threading.Timer(ShowCuurentDataTime, null, 0, 1000); //讓timer不能離開當前作用域 Console.ReadKey(); } public static void ShowCuurentDataTime(Object obj) { Console.WriteLine("[System.Threading.Timer]當前時間:" + DateTime.Now); //強制GC回收 GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); } } }
step 2:命令窗口中編譯
- 首先打開 " VS20XX 開發人員命令提示 ",可以在開始菜單的 VS 目錄下面找到,如下圖(用這個的原因是,這樣執行指令的時候,編輯環境由其准備好了),如下圖:
csc /t:exe /r:System.dll /Debug+ /optimize- program.cs
- 之后在目錄下面就會有一個exe生成,打開之后可以正常處理計時事件。
step 3:如果是在 Visual Studio IDE 下那么直接在工具欄上把 Debug 選上編譯運行,在 bin\debug 目錄下的會有 exe ,如下圖:
- 當然要想 Releas 編譯的程序能使定時器正常運作,這種方法是不行的;
- 使用其他類型的計時器,例如 System.Timers.Timer 或者是 System.Windows.Forms.Timer 。前者比較好用,后者會占用 UI 線程,不建議處理長時間的同步業務,如果寫成異步的,那么還是需要考慮異步的相關問題;
- 在程序結束之前增加計時器的應用根。一種是在退出,或者釋放承載了該種計時器的對象的時候,對計時器進行顯式的引用釋放,如
timer.Dispose();
值得注意的是,並不能使用
t=null;
這樣來引用,因為編輯器會優化代碼,而忽略這一行。還有一種方式就是把 Timer 設置為 Timer 承載對象的一個字段或者屬性,讓 Timer 的生命周期得以延長到承載對象的生命周期結束:
【1】在程序退出之前,對計時器進行顯式的引用釋放的方法,如下圖:
【2】使用字段或者屬性,延長Timer生命周期,如下圖: