首頁 >後端開發 >C#.Net教程 >詳解C#中Timer的使用與解決重入問題

詳解C#中Timer的使用與解決重入問題

黄舟
黄舟原創
2017-03-21 11:50:022225瀏覽

本文主要介紹了C#中Timer使用及解決重入問題的相關知識。具有很好的參考價值,下面跟著小編一起來看下吧

前言

#打開久違的Live Writer,又已經好久沒寫博客了,真的太懶了。廢話不多說了,直接進入這次部落格的主題--Timer。為什麼要寫這個呢,因為前幾天應朋友之邀,想做個「駭客」小工具,功能挺簡單就是自動取得剪貼簿的內容然後發送郵件,就需要用到Timer來循環取得剪貼簿的內容,但由於到了發送郵件這個功能,使用C#的SmtpClient始終發送不了郵件,以前寫過類似發郵件的功能,當時可以用網易的,現在也不能用了,不知道咋回事,只好作罷。在使用Timer中遇到了之前沒有想過的問題--重入。

介紹

首先簡單介紹timer,這裡所說的timer是指的System.Timers.timer,顧名思義,就是可以在指定的間隔是引發事件。官方介紹在這裡,摘抄如下:

Timer 元件是基於伺服器的計時器,它使您能夠指定在應用程式中引發 Elapsed 事件的周期性間隔。然後可透過處理這個事件來提供常規處理。 例如,假設您有一台關鍵性伺服器,必須每週 7 天、每天 24 小時保持運作。 可以建立一個使用 Timer 的服務,以定期檢查伺服器並確保系統開啟並正在運行。 如果系統不回應,則該服務可以嘗試重新啟動伺服器或通知管理員。 基於伺服器的 Timer 是為在多執行緒環境中用於輔助執行緒而設計的。 伺服器計時器可以在執行緒間移動來處理引發的 Elapsed 事件,這樣就可以比 Windows 計時器更精確地按時引發事件。

如果想了解跟其他的timer有啥區別,可以看這裡,裡面有詳細的介紹,不再多說了(其實我也不知道還有這麼多)。那使用這個計時器有啥好處呢?主要因為它是透過.NET Thread Pool實現的、輕量、計時精確、對應用程式及訊息沒有特別的要求。

使用

下面就簡單介紹一下,這個Timer是怎麼使用的,其實很簡單,我就採用微軟提供的範例來進行測試,直接上程式碼了:

//Timer不要声明成局部变量,否则会被GC回收
 private static System.Timers.Timer aTimer;
 public static void Main()
 {
 //实例化Timer类,设置间隔时间为10000毫秒; 
 aTimer = new System.Timers.Timer(10000);
 //注册计时器的事件
 aTimer.Elapsed += new ElapsedEventHandler(OnTimedEvent);
 //设置时间间隔为2秒(2000毫秒),覆盖构造函数设置的间隔
 aTimer.Interval = 2000;
 //设置是执行一次(false)还是一直执行(true),默认为true
 aTimer.AutoReset = true;
 //开始计时
 aTimer.Enabled = true;
 Console.WriteLine("按任意键退出程序。");
 Console.ReadLine();
 }
 //指定Timer触发的事件
 private static void OnTimedEvent(object source, ElapsedEventArgs e)
 {
 Console.WriteLine("触发的事件发生在: {0}", e.SignalTime);
 }

運行的結果如下,計時蠻準確的:

/*
按任意键退出程序。
触发的事件发生在: 2014/12/26 星期五 23:08:51
触发的事件发生在: 2014/12/26 星期五 23:08:53
触发的事件发生在: 2014/12/26 星期五 23:08:55
触发的事件发生在: 2014/12/26 星期五 23:08:57
触发的事件发生在: 2014/12/26 星期五 23:08:59
*/

重入問題重現及分析

什麼叫重入呢?這是一個有關多執行緒程式設計的概念:程式中,多個執行緒同時運行時,就可能發生同一個方法被多個行程同時呼叫的情況。當這個方法中存在一些非執行緒安全的程式碼時,方法重入會導致資料不一致的情況。 Timer方法重入是指使用多執行緒計時器,一個Timer處理還沒完成,到了時間,另一Timer還會繼續進入這個方法處理。下面示範一下重入問題的產生(可能重現的不是很好,不過也能簡單一下說明問題了):

//用来造成线程同步问题的静态成员
 private static int outPut = 1;
 //次数,timer没调一次方法自增1
 private static int num = 0;
 private static System.Timers.Timer timer = new System.Timers.Timer();
 public static void Main()
 {
 timer.Interval = 1000;
 timer.Elapsed += TimersTimerHandler;
 timer.Start();
 Console.WriteLine("按任意键退出程序。");
 Console.ReadLine();
 }
 /// <summary>
 /// System.Timers.Timer的回调方法
 /// </summary>
 /// <param name="sender"></param>
 /// <param name="args"></param>
 private static void TimersTimerHandler(object sender, EventArgs args)
 {
 int t = ++num;
 Console.WriteLine(string.Format("线程{0}输出:{1}, 输出时间:{2}", t, outPut.ToString(),DateTime.Now));
 System.Threading.Thread.Sleep(2000);
 outPut++;
 Console.WriteLine(string.Format("线程{0}自增1后输出:{1},输出时间:{2}", t, outPut.ToString(),DateTime.Now));
 }

下面顯示一下輸出結果:

是不是感覺上面輸出結果很奇怪,首先是線程1輸出為1,沒有問題,然後隔了2秒後線程1自增1後輸出為2,這就有問題了,中間為什麼還出現了線程2的輸出?更奇怪的是線程2剛開始輸出為1,自增1後盡然變成了3!其實這就是重入所導致的問題。別急,咱們分析一下就知道其中的緣由了。

首先timer啟動計時後,開啟一個執行緒1執行方法,當執行緒1第一次輸出之後,這時執行緒1休眠了2秒,此時timer並沒有閒著,因為設定的計時間隔為1秒,當在執行緒1休眠了1秒後,timer又開啟了執行緒2執行方法,執行緒2才不管執行緒1是執行中還是休眠狀態,所以此時執行緒2的輸出也為1,因為執行緒1還在休眠狀態,並沒有自增。然後又隔了1秒,這時發生同時發生兩個事件,線程1過了休眠狀態自增輸出為2,timer同時又開啟一個線程3,線程3輸出的為線程1自增後的值2,又過了1秒,線程2過了休眠狀態,之前的輸出已經是2,所以自增後輸出為3,又過了1秒……我都快暈了,大概就是這意思吧,我想表達的就是:一個Timer開啟的執行緒處理還沒完成,到了時間,另一Timer還會繼續進入這個方法處理。

那要怎麼解決這個問題呢?解決方案有三種,下面一一道來,適應不同的場景,但還是推薦最後一種,比較安全。

重入問題解決方案

1、使用lock(Object)的方法来防止重入,表示一个Timer处理正在执行,下一个Timer发生的时候发现上一个没有执行完就等待执行,适用重入很少出现的场景(具体也没研究过,可能比较占内存吧)。

代码跟上面差不多,在触发的方法中加入lock,这样当线程2进入触发的方法中,发现已经被锁,会等待锁中的代码处理完在执行,代码如下:

private static object locko = new object(); 
 /// <summary>
 /// System.Timers.Timer的回调方法
 /// </summary>
 /// <param name="sender"></param>
 /// <param name="args"></param>
 private static void TimersTimerHandler(object sender, EventArgs args)
 {
 int t = ++num; 
 lock (locko)
 { Console.WriteLine(string.Format("线程{0}输出:{1}, 输出时间:{2}", t, outPut.ToString(), DateTime.Now)); 
 System.Threading.Thread.Sleep(2000); 
 outPut++; Console.WriteLine(string.Format("线程{0}自增1后输出:{1},输出时间:{2}", t, outPut.ToString(), DateTime.Now)); } }

 执行结果:

 2、设置一个标志,表示一个Timer处理正在执行,下一个Timer发生的时候发现上一个没有执行完就放弃(注意这里是放弃,而不是等待哦,看看执行结果就明白啥意思了)执行,适用重入经常出现的场景。代码如下:

 private static int inTimer = 0; 
 /// <summary>
 /// System.Timers.Timer的回调方法
 /// </summary>
 /// <param name="sender"></param>
 /// <param name="args"></param>
 private static void TimersTimerHandler(object sender, EventArgs args)
 {
 int t = ++num;
 if (inTimer == 0)
 {
 inTimer = 1;
 Console.WriteLine(string.Format("线程{0}输出:{1}, 输出时间:{2}", t, outPut.ToString(), DateTime.Now));
 System.Threading.Thread.Sleep(2000);
 outPut++;
 Console.WriteLine(string.Format("线程{0}自增1后输出:{1},输出时间:{2}", t, outPut.ToString(), DateTime.Now));
 inTimer = 0;
 }
 }

执行结果:

3、在多线程下给inTimer赋值不够安全,Interlocked.Exchange提供了一种轻量级的线程安全的给对象赋值的方法(感觉比较高上大,也是比较推荐的一种方法),执行结果与方法2一样,也是放弃执行。Interlocked.Exchange用法参考这里。

private static int inTimer = 0; 
 /// <summary>
 /// System.Timers.Timer的回调方法
 /// </summary>
 /// <param name="sender"></param>
 /// <param name="args"></param>
 private static void TimersTimerHandler(object sender, EventArgs args)
 {
 int t = ++num;
 if (Interlocked.Exchange(ref inTimer, 1) == 0)
 {
 Console.WriteLine(string.Format("线程{0}输出:{1}, 输出时间:{2}", t, outPut.ToString(), DateTime.Now));
 System.Threading.Thread.Sleep(2000);
 outPut++;
 Console.WriteLine(string.Format("线程{0}自增1后输出:{1},输出时间:{2}", t, outPut.ToString(), DateTime.Now));
 Interlocked.Exchange(ref inTimer, 0); 
 }
 }

执行结果:

总结

终于码完字了,真心不容易啊。写博客是个挺耗精力的事情,真心佩服那些大牛们笔耕不辍,致敬!在这里稍微总结一下,timer是一个使用挺简单的类,拿来即用,这里主要总结了使用timer时重入问题的解决,以前也没思考过这个问题,解决方案也挺简单,在这里列出了三种,不知道还有没有其他的方式。这里的解决方案同时也适用多线程的重入问题。

以上是詳解C#中Timer的使用與解決重入問題的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn