標籤:set color and 通過 簡單的 布爾類型 類圖 cli 控制代碼
建議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:線上程同步中使用訊號量