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

來源:互聯網
上載者:User

標籤: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:線上程同步中使用訊號量

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.