C#中的多線程逾時處理實踐

來源:互聯網
上載者:User

標籤:簡單   bool   operation   事件   ack   線程池   on()   tps   col   

最近我正在處理C#中關於timeout行為的一些bug。解決方案非常有意思,所以我在這裡分享給廣大博友們。

我要處理的是下面這些情況:

  • 我們做了一個應用程式,程式中有這麼一個模組,它的功能向使用者顯示一個訊息對話方塊,15秒後再自動關閉該對話方塊。但是,如果使用者手動關閉對話方塊,則在timeout時我們無需做任何處理。

  • 程式中有一個漫長的執行操作。如果該操作持續5秒鐘以上,那麼請終止這個操作。

  • 我們的的應用程式中有執行時間未知的操作。當執行時間過長時,我們需要顯示一個“進行中”快顯視窗來提示使用者耐心等待。我們無法預估這次操作會持續多久,但一般情況下會持續不到一秒。為了避免快顯視窗一閃而過,我們只想要在1秒後顯示這個快顯視窗。反之,如果在1秒內操作完成,則不需要顯示這個快顯視窗。

這些問題是相似的。在逾時之後,我們必須執行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"     }}

我的操作類:

public class MyOperation : IOperation{    public void DoOperation()    {        Console.WriteLine("Operation started");    }}public class MyOperation : IOperation{    public void DoOperation()    {        Console.WriteLine("Operation started");    }}

我的測試程式:

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();}

結果應該是:


Starting with timeout of 5 seconds
Operation started
Starting with timeout of 5 but cancelling after 2 seconds
Finished...

現在我們可以開始實驗了!

解決方案1:在另一個線程上休眠

我最初的計劃是在另一個不同的線程上休眠,同時用一個布爾值來標記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的調用。

如果我們嘗試使用計時器呢?

解決方案2:使用計時器

.NET中有三種不同類型的記時器,分別是:

  • System.Windows.Forms命名空間下的Timer控制項,它直接繼承自Componet。
  • System.Timers命名空間下的Timer類。
  • System.Threading.Timer類。

這三種計時器中,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...

現在當我們停止操作時,定時器被丟棄,這樣就避免了再次執行操作。這已經實現了我們最初的想法,當然還有另一種方式來處理這個問題。

解決方案3:ManualResetEvent或AutoResetEvent

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. 在以前的StartWithTimeout逾時期間調用StartWithTimeout時:忽略第二次啟動。
  2. 在以前的StartWithTimeout逾時期間調用StartWithTimeout時:停止初始話Start並使用新的StartWithTimeout。
  3. 在以前的StartWithTimeout逾時期間調用StartWithTimeout時:在兩個啟動中調用DoOperation。 在StopOperationIfNotStartedYet中停止所有尚未開始的操作(在逾時時間內)。
  4. 在以前的StartWithTimeout逾時期間調用StartWithTimeout時:在兩個啟動中調用DoOperation。 在StopOperationIfNotStartedYet停止一個尚未開始的隨機操作。

可能性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是非常強大的類,我在處理線程通訊時一直使用它們。這兩個類非常實用。正在跟線程通訊打交道的朋友們,快把它們加入到項目裡面吧!

C#中的多線程逾時處理實踐

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.