最近我正在處理C#中關於timeout行爲的一些bug。解決方案很是有意思,因此我在這裏分享給廣大博友們。多線程
我要處理的是下面這些狀況:測試
咱們作了一個應用程序,程序中有這麼一個模塊,它的功能向用戶顯示一個消息對話框,15秒後再自動關閉該對話框。可是,若是用戶手動關閉對話框,則在timeout時咱們無需作任何處理。線程
程序中有一個漫長的執行操做。若是該操做持續5秒鐘以上,那麼請終止這個操做。code
咱們的的應用程序中有執行時間未知的操做。當執行時間過長時,咱們須要顯示一個「進行中」彈出窗口來提示用戶耐心等待。咱們沒法預估此次操做會持續多久,但通常狀況下會持續不到一秒。爲了不彈出窗口一閃而過,咱們只想要在1秒後顯示這個彈出窗口。反之,若是在1秒內操做完成,則不須要顯示這個彈出窗口。orm
這些問題是類似的。在超時以後,咱們必須執行X操做,除非Y在那個時候發生。繼承
爲了找到解決這些問題的辦法,我在試驗過程當中建立了一個類:事件
public class OperationHandler { private IOperation _operation; public OperationHandler(IOperation operation) { _operation = operation; } public void StartWithTimeout(int timeoutMillis) { //在超時後須要調用 "_operation.DoOperation()" } public void StopOperationIfNotStartedYet() { //在超時期間須要中止"DoOperation" } }
個人操做類:get
public class MyOperation : IOperation { public void DoOperation() { Console.WriteLine("Operation started"); } } public class MyOperation : IOperation { public void DoOperation() { Console.WriteLine("Operation started"); } }
個人測試程序:string
static void Main(string[] args) { var op = new MyOperation(); var handler = new OperationHandler(op); Console.WriteLine("Starting with timeout of 5 seconds"); handler.StartWithTimeout(5 * 1000); Thread.Sleep(6 * 1000); Console.WriteLine("Starting with timeout of 5 but cancelling after 2 seconds"); handler.StartWithTimeout(5 * 1000); Thread.Sleep(2 * 1000); handler.StopOperationIfNotStartedYet(); Thread.Sleep(4 * 1000); Console.WriteLine("Finished..."); Console.ReadLine(); }
結果應該是:
it
Starting with timeout of 5 seconds Operation started Starting with timeout of 5 but cancelling after 2 seconds Finished... |
如今咱們能夠開始試驗了!
我最初的計劃是在另外一個不一樣的線程上休眠,同時用一個布爾值來標記Stop是否被調用。
public class OperationHandler { private IOperation _operation; private bool _stopCalled; public OperationHandler(IOperation operation) { _operation = operation; } public void StartWithTimeout(int timeoutMillis) { Task.Factory.StartNew(() => { _stopCalled = false; Thread.Sleep(timeoutMillis); if (!_stopCalled) _operation.DoOperation(); }); } public void StopOperationIfNotStartedYet() { _stopCalled = true; } }
針對正常的線程執行步驟,這段代碼運行過程並無出現問題,可是老是感受有些彆扭。仔細探究後,我發現其中有一些貓膩。首先,在超時期間,有一個線程從線程池中取出後什麼都沒作,顯然這個線程是被浪費了。其次,若是程序中止執行了,線程會繼續休眠直到超時結束,浪費了CPU時間。
可是這些並非咱們這段代碼最糟糕的事情,實際上咱們的程序實還存在一個明顯的bug:
若是咱們設置10秒的超時時間,開始操做後,2秒中止,而後在2秒內再次開始。
當第二次啓動時,咱們的_stopCalled標誌將變成false。而後,當咱們的第一個Thread.Sleep()完成時,即便咱們取消它,它也會調用DoOperation。
以後,第二個Thread.Sleep()完成,並將第二次調用DoOperation。結果致使DoOperation被調用兩次,這顯然不是咱們所指望的。
若是你每分鐘有100次這樣的超時,我將很難捕捉到這種錯誤。
當StopOperationIfNotStartedYet被調用時,咱們須要某種方式來取消DoOperation的調用。
若是咱們嘗試使用計時器呢?
.NET中有三種不一樣類型的記時器,分別是:
這三種計時器中,System.Threading.Timer足以知足咱們的需求。這裏是使用Timer的代碼:
public class OperationHandler { private IOperation _operation; private Timer _timer; public OperationHandler(IOperation operation) { _operation = operation; } public void StartWithTimeout(int timeoutMillis) { if (_timer != null) return; _timer = new Timer( state => { _operation.DoOperation(); DisposeOfTimer(); }, null, timeoutMillis, timeoutMillis); } public void StopOperationIfNotStartedYet() { DisposeOfTimer(); } private void DisposeOfTimer() { if (_timer == null) return; var temp = _timer; _timer = null; temp.Dispose(); } }
執行結果以下:
Starting with timeout of 5 seconds Operation started Starting with timeout of 5 but cancelling after 2 seconds Finished... |
如今當咱們中止操做時,定時器被丟棄,這樣就避免了再次執行操做。這已經實現了咱們最初的想法,固然還有另外一種方式來處理這個問題。
ManualResetEvent/AutoResetEvent的字面意思是手動或自動重置事件。AutoResetEvent和ManualResetEvent是幫助您處理多線程通訊的類。 基本思想是一個線程能夠一直等待,知道另外一個線程完成某個操做, 而後等待的線程能夠「釋放」並繼續運行。
ManualResetEvent類和AutoResetEvent類請參閱MSDN:
ManualResetEvent類:https://msdn.microsoft.com/zh-cn/library/system.threading.manualresetevent.aspx
AutoResetEvent類:https://msdn.microsoft.com/zh-cn/library/system.threading.autoresetevent.aspx
言歸正傳,在本例中,直到手動重置事件信號出現,mre.WaitOne()會一直等待。 mre.Set()將標記重置事件信號。 ManualResetEvent將釋放當前正在等待的全部線程。AutoResetEvent將只釋放一個等待的線程,並當即變爲無信號。WaitOne()也能夠接受超時做爲參數。 若是Set()在超時期間未被調用,則線程被釋放而且WaitOne()返回False。
如下是此功能的實現代碼:
public class OperationHandler { private IOperation _operation; private ManualResetEvent _mre = new ManualResetEvent(false); public OperationHandler(IOperation operation) { _operation = operation; } public void StartWithTimeout(int timeoutMillis) { _mre.Reset(); Task.Factory.StartNew(() => { bool wasStopped = _mre.WaitOne(timeoutMillis); if (!wasStopped) _operation.DoOperation(); }); } public void StopOperationIfNotStartedYet() { _mre.Set(); } }
執行結果:
Starting with timeout of 5 seconds Operation started Starting with timeout of 5 but cancelling after 2 seconds Finished... |
我我的很是傾向於這個解決方案,它比咱們使用Timer的解決方案更乾淨簡潔。
對於咱們提出的簡單功能,ManualResetEvent和Timer解決方案均可以正常工做。 如今讓咱們增長點挑戰性。
新的改進需求
假設咱們如今能夠連續屢次調用StartWithTimeout(),而不是等待第一個超時完成後調用。
可是這裏的預期行爲是什麼?實際上存在如下幾種可能性:
可能性1能夠經過Timer和ManualResetEvent能夠輕鬆實現。 事實上,咱們已經在咱們的Timer解決方案中涉及到了這個。
public void StartWithTimeout(int timeoutMillis) { if (_timer != null) return; ... public void StartWithTimeout(int timeoutMillis) { if (_timer != null) return; ... }
可能性2也能夠很容易地實現。 這個地方請容許我賣個萌,代碼本身寫哈^_^
可能性3不可能經過使用Timer來實現。 咱們將須要有一個定時器的集合。 一旦中止操做,咱們須要檢查並處理定時器集合中的全部子項。 這種方法是可行的,但經過ManualResetEvent咱們能夠很是簡潔和輕鬆的實現這一點!
可能性4跟可能性3類似,能夠經過定時器的集合來實現。
可能性3:使用單個ManualResetEvent中止全部操做
讓咱們瞭解一下這裏面遇到的難點:
假設咱們調用StartWithTimeout 10秒超時。
1秒後,咱們再次調用另外一個StartWithTimeout,超時時間爲10秒。
再過1秒後,咱們再次調用另外一個StartWithTimeout,超時時間爲10秒。
預期的行爲是這3個操做會依次10秒、11秒和12秒後啓動。
若是5秒後咱們會調用Stop(),那麼預期的行爲就是全部正在等待的操做都會中止, 後續的操做也沒法進行。
我稍微改變下Program.cs,以便可以測試這個操做過程。 這是新的代碼:
class Program { static void Main(string[] args) { var op = new MyOperation(); var handler = new OperationHandler(op); Console.WriteLine("Starting with timeout of 10 seconds, 3 times"); handler.StartWithTimeout(10 * 1000); Thread.Sleep(1000); handler.StartWithTimeout(10 * 1000); Thread.Sleep(1000); handler.StartWithTimeout(10 * 1000); Thread.Sleep(13 * 1000); Console.WriteLine("Starting with timeout of 10 seconds 3 times, but cancelling after 5 seconds"); handler.StartWithTimeout(10 * 1000); Thread.Sleep(1000); handler.StartWithTimeout(10 * 1000); Thread.Sleep(1000); handler.StartWithTimeout(10 * 1000); Thread.Sleep(5 * 1000); handler.StopOperationIfNotStartedYet(); Thread.Sleep(8 * 1000); Console.WriteLine("Finished..."); Console.ReadLine(); } }
下面就是使用ManualResetEvent的解決方案:
public class OperationHandler { private IOperation _operation; private ManualResetEvent _mre = new ManualResetEvent(false); public OperationHandler(IOperation operation) { _operation = operation; } public void StartWithTimeout(int timeoutMillis) { Task.Factory.StartNew(() => { bool wasStopped = _mre.WaitOne(timeoutMillis); if (!wasStopped) _operation.DoOperation(); }); } public void StopOperationIfNotStartedYet() { Task.Factory.StartNew(() => { _mre.Set(); Thread.Sleep(10);//This is necessary because if calling Reset() immediately, not all waiting threads will 'proceed' _mre.Reset(); }); } }
輸出結果跟預想的同樣:
Starting with timeout of 10 seconds, 3 times Operation started Operation started Operation started Starting with timeout of 10 seconds 3 times, but cancelling after 5 seconds Finished... |
很開森對不對?
當我檢查這段代碼時,我發現Thread.Sleep(10)是必不可少的,這顯然超出了個人意料。 若是沒有它,除3個等待中的線程以外,只有1-2個線程正在進行。 很明顯的是,由於Reset()發生得太快,第三個線程將停留在WaitOne()上。
可能性4:單個AutoResetEvent中止一個隨機操做
假設咱們調用StartWithTimeout 10秒超時。1秒後,咱們再次調用另外一個StartWithTimeout,超時時間爲10秒。再過1秒後,咱們再次調用另外一個StartWithTimeout,超時時間爲10秒。而後咱們調用StopOperationIfNotStartedYet()。
目前有3個操做超時,等待啓動。 預期的行爲是其中一個被中止, 其餘2個操做應該可以正常啓動。
咱們的Program.cs能夠像之前同樣保持不變。 OperationHandler作了一些調整:
public class OperationHandler { private IOperation _operation; private AutoResetEvent _are = new AutoResetEvent(false); public OperationHandler(IOperation operation) { _operation = operation; } public void StartWithTimeout(int timeoutMillis) { _are.Reset(); Task.Factory.StartNew(() => { bool wasStopped = _are.WaitOne(timeoutMillis); if (!wasStopped) _operation.DoOperation(); }); } public void StopOperationIfNotStartedYet() { _are.Set(); } }
執行結果是:
Starting with timeout of 10 seconds, 3 times Operation started Operation started Operation started Starting with timeout of 10 seconds 3 times, but cancelling after 5 seconds Operation started Operation started Finished... |
在處理線程通訊時,超時後繼續執行某些操做是常見的應用。咱們嘗試了一些很好的解決方案。一些解決方案可能看起來不錯,甚至能夠在特定的流程下工做,可是也有可能在代碼中隱藏着致命的bug。當這種狀況發生時,咱們應對時須要特別當心。
AutoResetEvent和ManualResetEvent是很是強大的類,我在處理線程通訊時一直使用它們。這兩個類很是實用。正在跟線程通訊打交道的朋友們,快把它們加入到項目裏面吧!