1. 程式人生 > >【轉】編寫高質量代碼改善C#程序的157個建議——建議72:在線程同步中使用信號量

【轉】編寫高質量代碼改善C#程序的157個建議——建議72:在線程同步中使用信號量

obj void 在線 需要 接收 bsp 連接斷開 否則 繼續

建議72:在線程同步中使用信號量

所謂線程同步,就是多個線程在某個對象上執行等待(也可理解為鎖定該對象),直到該對象被解除鎖定。C#中對象的類型分為引用類型和值類型。CLR在這兩種類型上的等待是不一樣的。我們可以簡單地理解為在CLR中,值類型是不能被鎖定的,即不能在一個值類型對象上執行等待。而在引用類型上的等待機制,又分為兩類:鎖定和信號同步。

鎖定使用關鍵字lock和類型Monitor。兩者沒有實質區別,前者其實是後者的語法糖。這是最常用的同步技術。

本建議主要討論信號同步。信號同步機制中涉及的類型都繼承自抽象類WaitHandle,這些類型有EventWaitHandle(類型化為AutoResetEvent、ManualResetEvent)、Semaphore以及Mutex。見類圖6-3。

技術分享圖片


圖6-3 同步功能類的類圖
EventWaitHandle(子類為AutoResetEvent、ManualResetEvent)、Semaphore以及Mutex都繼承自WaitHandle,所以它們底層的原理是一致的,維護的都是一個系統內核句柄。不過我們仍需簡單地區分這三個類的類型。

EventWaitHandle維護一個由內核產生的布爾類型對象(稱為“阻滯狀態”),如果其值為false,那麽在它上面等待的線程就阻塞。可以調用類型的Set方法將其值設置為true,解除阻塞。EventWaitHandle類型有兩個子類AutoResetEvent和ManualResetEvent,它們的區別並不大,本建議接下來會針對它們闡述如何正確使用信號量。

Semaphore維護一個由內核產生的整型變量,如果其值為0,則在它上面等待的線程就會阻塞;如果其值大於0,則解除阻塞,同時,每解除一個線程阻塞,其值就減1。

EventWaitHandle和Semaphore提供的都是單應用程序域內的線程同步功能,Mutex則不同,它為我們提供了跨應用程序域阻塞和解除阻塞線程的能力。

使用信號機制提供線程同步的一個簡單例子如下所示:

AutoResetEvent autoResetEvent = new AutoResetEvent(false);  
 
private void buttonStartAThread_Click(object sender, EventArgs e)  
{  
    Thread tWork = new Thread(() =>
    {  
        label1.Text = "線程啟動..." + Environment.NewLine;  
        label1.Text += "開始處理一些實際的工作" + Environment.NewLine;  
        
//省略工作代碼 label1.Text += "我開始等待別的線程給我信號,才願意繼續下去" + Environment.NewLine; autoResetEvent.WaitOne(); label1.Text += "我繼續做一些工作,然後結束了!"; //省略工作代碼 }); tWork.IsBackground = true; tWork.Start(); } private void buttonSet_Click(object sender, EventArgs e) { //給在autoResetEvent上等待的線程一個信號 autoResetEvent.Set(); }


這是一個簡單的Winform窗體程序,其中一個按鈕負責開啟一個新的線程,另一個按鈕負責給剛開啟的那個線程發送信號。現在詳細解釋其中發生的事情。

AutoResetEvent autoResetEvent = new AutoResetEvent(false); 

這段代碼創建了一個同步類型對象autoResetEvent,它設置自己的默認阻滯狀態是false。這意味著任何在它上面進行等待的線程都將被阻滯。所謂等待,就是在線程中應用:

autoResetEvent.WaitOne(); 

這說明tWork開始在autoResetEvent上等待任何其他地方給它的信號。信號來了,則tWork開始繼續工作,否則就一直等著(即阻滯)。接下來看看主線程中的這句代碼(本例中即UI線程,它相對於線程tWork來說,就是一個“另外的線程”):

autoResetEvent.Set(); 

主線程通過上面這句代碼向在autoResetEvent上等待的線程tWork上下文發送信號,即將tWork的阻滯狀態設置為true。tWork接收到這個信號後,開始繼續工作。 這個例子相當簡單,但是已經完整說明了信號機制的工作原理。 AutoResetEvent和ManualResetEvent的區別是:前者在發送信號完畢後(即調用Set方法),會自動將自己的阻滯狀態設置為false,而後者則需要進行手動設定。通過一個例子來說明這種區別,如下所示:

AutoResetEvent autoResetEvent = new AutoResetEvent(false);  
 
private void buttonStartAThread_Click(object sender, EventArgs e)  
{  
    StartThread1();  
    StartThread2();  
}  
 
private void StartThread1()  
{  
    Thread tWork1 = new Thread(() =>
    {  
        label1.Text = "線程1啟動..." + Environment.NewLine;  
        label1.Text += "開始處理一些實際的工作" + Environment.NewLine;  
        //省略工作代碼  
        label1.Text += "我開始等待別的線程給我信號,才願意繼續下去" + Environment.NewLine;  
        autoResetEvent.WaitOne();  
        label1.Text += "我繼續做一些工作,然後結束了!";  
        //省略工作代碼  
    });  
    tWork1.IsBackground = true;  
    tWork1.Start();  
}  
 
private void StartThread2()  
{  
    Thread tWork2 = new Thread(() =>
    {  
        label2.Text = "線程2啟動..." + Environment.NewLine;  
        label2.Text += "開始處理一些實際的工作" + Environment.NewLine;  
        //省略工作代碼  
        label2.Text += "我開始等待別的線程給我信號,才願意繼續下去" + Environment.NewLine;  
        autoResetEvent.WaitOne();  
        label2.Text += "我繼續做一些工作,然後結束了!";  
        //省略工作代碼  
    });  
    tWork2.IsBackground = true;  
    tWork2.Start();  
}  
 
private void buttonSet_Click(object sender, EventArgs e)  
{  
    //給在autoResetEvent上等待的線程一個信號  
    autoResetEvent.Set();  
} 


這個例子的本意是要讓新起的兩個工作線程tWork1和tWork2都阻滯,直到收到主線程的信號再繼續工作。而程序運行的結果是,只有一個工作線程繼續工作,另外一個工作線程則繼續保持阻滯狀態。我想可能大家都已經猜到原因了,即AutoResetEvent發送信號完畢就在內核中自動將自己的狀態設置回false了,所以另外一個工作線程相當於根本沒有收到主線程的信號。

要修正這個問題,可以使用ManualResetEvent。大家可以將其換成ManualResetEvent試一下。

最後,再舉一個需要用到線程同步的實際例子:模擬網絡通信。客戶端在運行過程中,服務器每隔一段的時間會給客戶端發送心跳數據。實際工作中的服務器和客戶端在網絡中是兩臺不同的終端,不過在這個例子中我們將其進行了簡化:工作線程tClient模擬客戶端,主線程(UI線程)模擬服務器端。客戶端每3秒檢測是否收到服務器的心跳數據,如果沒有心跳數據,則顯示網絡連接斷開。代碼如下所示:

AutoResetEvent autoResetEvent = new AutoResetEvent(false);  
 
private void buttonStartAThread_Click(object sender, EventArgs e)  
{  
    Thread tClient = new Thread(() =>
        {  
            while (true)  
            {  
                //等3秒,3秒沒有信號,顯示斷開  
                //有信號,則顯示更新  
                bool re = autoResetEvent.WaitOne(3000);  
                if (re)  
                {  
                    label1.Text = string.Format("時間:{0},{1}",   
                        DateTime.Now.ToString(), "保持連接狀態");  
                }  
                else  
                {  
                    label1.Text = string.Format("時間:{0},{1}",   
                        DateTime.Now.ToString(), "斷開,需要重啟");  
                }  
            }  
        });  
    tClient.IsBackground = true;  
    tClient.Start();  
}  
 
private void buttonSet_Click(object sender, EventArgs e)  
{  
    //模擬發送心跳數據  
    autoResetEvent.Set();  
} 

轉自:《編寫高質量代碼改善C#程序的157個建議》陸敏技

【轉】編寫高質量代碼改善C#程序的157個建議——建議72:在線程同步中使用信號量