生活随笔
收集整理的這篇文章主要介紹了
[ZT]C#的多线程机制探索(2)
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
三 . 線程的同步和通訊 —— 生產者和消費者 假設這樣一種情況,兩個線程同時維護一個隊列,如果一個線程對隊列中添加元素,而另外一個線程從隊列中取用元素,那么我們稱添加元素的線程為生產者,稱取用元素的線程為消費者。生產者與消費者問題看起來很簡單,但是卻是多線程應用中一個必須解決的問題,它涉及到線程之間的同步和通訊問題。 前面說過,每個線程都有自己的資源,但是代碼區是共享的,即每個線程都可以執行相同的函數。但是多線程環境下,可能帶來的問題就是幾個線程同時執行一個函數,導致數據的混亂,產生不可預料的結果,因此我們必須避免這種情況的發生。 C# 提供了一個關鍵字 lock ,它可以把一段代碼定義為互斥段( critical section ),互斥段在一個時刻內只允許一個線程進入執行,而其他線程必須等待。在 C# 中,關鍵字 lock 定義如下: lock(expression) statement_block
expression 代表你希望跟蹤的對象,通常是對象引用。一般地,如果你想保護一個類的實例,你可以使用 this ;如果你希望保護一個靜態變量(如互斥代碼段在一個靜態方法內部),一般使用類名就可以了。而 statement_block 就是互斥段的代碼,這段代碼在一個時刻內只可能被一個線程執行。 下面是一個使用 lock 關鍵字的典型例子,我將在注釋里向大家說明 lock 關鍵字的用法和用途:
//lock.cs using System; using System.Threading; internal class Account { int balance; Random r = new Random(); internal Account(int initial) { balance = initial; } internal int Withdraw(int amount) { if (balance < 0) { file:// 如果 balance 小于 0 則拋出異常 throw new Exception("Negative Balance"); } // 下面的代碼保證在當前線程修改 balance 的值完成之前 // 不會有其他線程也執行這段代碼來修改 balance 的值 // 因此, balance 的值是不可能小于 0 的 lock (this) { Console.WriteLine("Current Thread:"+Thread.CurrentThread.Name); file:// 如果沒有 lock 關鍵字的保護,那么可能在執行完 if 的條件判斷之后 file:// 另外一個線程卻執行了 balance=balance-amount 修改了 balance 的值 file:// 而這個修改對這個線程是不可見的,所以可能導致這時 if 的條件已經不成立了 file:// 但是,這個線程卻繼續執行 balance=balance-amount ,所以導致 balance 可能小于 0 if (balance >= amount) { Thread.Sleep(5); balance = balance - amount; return amount; } else { return 0; // transaction rejected } } } internal void DoTransactions() { for (int i = 0; i < 100; i++) Withdraw(r.Next(-50, 100)); } } internal class Test { static internal Thread[] threads = new Thread[10]; public static void Main() { Account acc = new Account (0); for (int i = 0; i < 10; i++) { Thread t = new Thread(new ThreadStart(acc.DoTransactions)); threads[i] = t; } for (int i = 0; i < 10; i++) threads[i].Name=i.ToString(); for (int i = 0; i < 10; i++) threads[i].Start(); Console.ReadLine(); } }
而多線程公用一個對象時,也會出現和公用代碼類似的問題,這種問題就不應該使用 lock 關鍵字了,這里需要用到 System.Threading 中的一個類 Monitor ,我們可以稱之為監視器, Monitor 提供了使線程共享資源的方案。 Monitor 類可以鎖定一個對象,一個線程只有得到這把鎖才可以對該對象進行操作。對象鎖機制保證了在可能引起混亂的情況下一個時刻只有一個線程可以訪問這個對象。 Monitor 必須和一個具體的對象相關聯,但是由于它是一個靜態的類,所以不能使用它來定義對象,而且它的所有方法都是靜態的,不能使用對象來引用。下面代碼說明了使用 Monitor 鎖定一個對象的情形:
...... Queue oQueue=new Queue(); ...... Monitor.Enter(oQueue); ......// 現在 oQueue 對象只能被當前線程操縱了 Monitor.Exit(oQueue);//釋放鎖
如上所示,當一個線程調用 Monitor.Enter() 方法鎖定一個對象時,這個對象就歸它所有了,其它線程想要訪問這個對象,只有等待它使用 Monitor.Exit() 方法釋放鎖。為了保證線程最終都能釋放鎖,你可以把 Monitor.Exit() 方法寫在 try-catch-finally 結構中的 finally 代碼塊里。對于任何一個被 Monitor 鎖定的對象,內存中都保存著與它相關的一些信息,其一是現在持有鎖的線程的引用,其二是一個預備隊列,隊列中保存了已經準備好獲取鎖的線程,其三是一個等待隊列,隊列中保存著當前正在等待這個對象狀態改變的隊列的引用。當擁有對象鎖的線程準備釋放鎖時,它使用 Monitor.Pulse() 方法通知等待隊列中的第一個線程,于是該線程被轉移到預備隊列中,當對象鎖被釋放時,在預備隊列中的線程可以立即獲得對象鎖。 下面是一個展示如何使用 lock 關鍵字和 Monitor 類來實現線程的同步和通訊的例子,也是一個典型的生產者與消費者問題。這個例程中,生產者線程和消費者線程是交替進行的,生產者寫入一個數,消費者立即讀取并且顯示,我將在注釋中介紹該程序的精要所在。用到的系統命名空間如下:
using System; using System.Threading;
首先,我們定義一個被操作的對象的類 Cell ,在這個類里,有兩個方法: ReadFromCell() 和 WriteToCell 。消費者線程將調用 ReadFromCell() 讀取 cellContents 的內容并且顯示出來,生產者進程將調用 WriteToCell() 方法向 cellContents 寫入數據。
public class Cell { int cellContents; // Cell 對象里邊的內容 bool readerFlag = false; // 狀態標志,為 true 時可以讀取,為 false 則正在寫入 public int ReadFromCell( ) { lock(this) // Lock 關鍵字保證了什么,請大家看前面對 lock 的介紹 { if (!readerFlag)// 如果現在不可讀取 { try { file:// 等待 WriteToCell 方法中調用 Monitor.Pulse() 方法 Monitor.Wait(this); } catch (SynchronizationLockException e) { Console.WriteLine(e); } catch (ThreadInterruptedException e) { Console.WriteLine(e); } } Console.WriteLine("Consume: {0}",cellContents); readerFlag = false; file:// 重置 readerFlag 標志,表示消費行為已經完成 Monitor.Pulse(this); file:// 通知 WriteToCell() 方法(該方法在另外一個線程中執行,等待中) } return cellContents; } public void WriteToCell(int n) { lock(this) { if (readerFlag) { try { Monitor.Wait(this); } catch (SynchronizationLockException e) { file:// 當同步方法(指 Monitor 類除 Enter 之外的方法)在非同步的代碼區被調用 Console.WriteLine(e); } catch (ThreadInterruptedException e) { file:// 當線程在等待狀態的時候中止 Console.WriteLine(e); } } cellContents = n; Console.WriteLine("Produce: {0}",cellContents); readerFlag = true; Monitor.Pulse(this); file:// 通知另外一個線程中正在等待的 ReadFromCell() 方法 } } }
下面定義生產者 CellProd 和消費者類 CellCons ,它們都只有一個方法 ThreadRun() ,以便在 Main() 函數中提供給線程的 ThreadStart 代理對象,作為線程的入口。
public class CellProd { Cell cell; // 被操作的 Cell 對象 int quantity = 1; // 生產者生產次數,初始化為 1 public CellProd(Cell box, int request) { // 構造函數 cell = box; quantity = request; } public void ThreadRun( ) { for(int looper=1; looper<=quantity; looper++) cell.WriteToCell(looper); file:// 生產者向操作對象寫入信息 } } public class CellCons { Cell cell; int quantity = 1; public CellCons(Cell box, int request) { cell = box; quantity = request; } public void ThreadRun( ) { int valReturned; for(int looper=1; looper<=quantity; looper++) valReturned=cell.ReadFromCell( );// 消費者從操作對象中讀取信息 } }
然后在下面這個類 MonitorSample 的 Main() 函數中我們要做的就是創建兩個線程分別作為生產者和消費者,使用 CellProd.ThreadRun() 方法和 CellCons.ThreadRun() 方法對同一個 Cell 對象進行操作。
public class MonitorSample { public static void Main(String[] args) { int result = 0; file:// 一個標志位,如果是 0 表示程序沒有出錯,如果是 1 表明有錯誤發生 Cell cell = new Cell( ); // 下面使用 cell 初始化 CellProd 和 CellCons 兩個類,生產和消費次數均為 20 次 CellProd prod = new CellProd(cell, 20); CellCons cons = new CellCons(cell, 20); Thread producer = new Thread(new ThreadStart(prod.ThreadRun)); Thread consumer = new Thread(new ThreadStart(cons.ThreadRun)); // 生產者線程和消費者線程都已經被創建,但是沒有開始執行 try { producer.Start( ); consumer.Start( ); producer.Join( ); consumer.Join( ); Console.ReadLine(); } catch (ThreadStateException e) { file:// 當線程因為所處狀態的原因而不能執行被請求的操作 Console.WriteLine(e); result = 1; } catch (ThreadInterruptedException e) { file:// 當線程在等待狀態的時候中止 Console.WriteLine(e); result = 1; } // 盡管 Main() 函數沒有返回值,但下面這條語句可以向父進程返回執行結果 Environment.ExitCode = result; } }
大家可以看到,在上面的例程中,同步是通過等待 Monitor.Pulse() 來完成的。首先生產者生產了一個值,而同一時刻消費者處于等待狀態,直到收到生產者的 “ 脈沖 (Pulse)” 通知它生產已經完成,此后消費者進入消費狀態,而生產者開始等待消費者完成操作后將調用 Monitor.Pulese() 發出的 “ 脈沖 ” 。它的執行結果很簡單:
Produce: 1 Consume: 1 Produce: 2 Consume: 2 Produce: 3 Consume: 3 ... ... Produce: 20 Consume: 20
事實上,這個簡單的例子已經幫助我們解決了多線程應用程序中可能出現的大問題,只要領悟了解決線程間沖突的基本方法,很容易把它應用到比較復雜的程序中去。
轉載于:https://www.cnblogs.com/supersand/archive/2005/08/27/224185.html
總結
以上是生活随笔 為你收集整理的[ZT]C#的多线程机制探索(2) 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。