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