C#多線程學(xué)習(xí)之(三)生產(chǎn)者和消費者用法分析
本文實例講述了C#多線程學(xué)習(xí)之生產(chǎn)者和消費者用法。分享給大家供大家參考。具體實分析如下:
前面的文章說過,每個線程都有自己的資源,但是代碼區(qū)是共享的,即每個線程都可以執(zhí)行相同的函數(shù)。這可能帶來的問題就是幾個線程同時執(zhí)行一個函數(shù),導(dǎo)致數(shù)據(jù)的混亂,產(chǎn)生不可預(yù)料的結(jié)果,因此我們必須避免這種情況的發(fā)生。
C#提供了一個關(guān)鍵字lock,它可以把一段代碼定義為互斥段(critical section),互斥段在一個時刻內(nèi)只允許一個線程進(jìn)入執(zhí)行,而其他線程必須等待。在C#中,關(guān)鍵字lock定義如下:
lock(expression) statement_block
expression代表你希望跟蹤的對象,通常是對象引用。
如果你想保護(hù)一個類的實例,一般地,你可以使用this;
如果你想保護(hù)一個靜態(tài)變量(如互斥代碼段在一個靜態(tài)方法內(nèi)部),一般使用類名就可以了。
而statement_block就是互斥段的代碼,這段代碼在一個時刻內(nèi)只可能被一個線程執(zhí)行。
下面是一個使用lock關(guān)鍵字的典型例子,在注釋里說明了lock關(guān)鍵字的用法和用途。
示例如下:
using System; using System.Threading; namespace ThreadSimple { internal class Account { int balance; Random r = new Random(); internal Account(int initial) { balance = initial; } internal int Withdraw(int amount) { if (balance < 0) { //如果balance小于0則拋出異常 throw new Exception("Negative Balance"); } //下面的代碼保證在當(dāng)前線程修改balance的值完成之前 //不會有其他線程也執(zhí)行這段代碼來修改balance的值 //因此,balance的值是不可能小于0 的 lock (this) { Console.WriteLine("Current Thread:"+Thread.CurrentThread.Name); //如果沒有l(wèi)ock關(guān)鍵字的保護(hù),那么可能在執(zhí)行完if的條件判斷之后 //另外一個線程卻執(zhí)行了balance=balance-amount修改了balance的值 //而這個修改對這個線程是不可見的,所以可能導(dǎo)致這時if的條件已經(jīng)不成立了 //但是,這個線程卻繼續(xù)執(zhí)行balance=balance-amount,所以導(dǎo)致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(); } } }
Monitor 類鎖定一個對象
當(dāng)多線程公用一個對象時,也會出現(xiàn)和公用代碼類似的問題,這種問題就不應(yīng)該使用lock關(guān)鍵字了,這里需要用到System.Threading中的一個類Monitor,我們可以稱之為監(jiān)視器,Monitor提供了使線程共享資源的方案。
Monitor類可以鎖定一個對象,一個線程只有得到這把鎖才可以對該對象進(jìn)行操作。對象鎖機制保證了在可能引起混亂的情況下一個時刻只有一個線程可以訪問這個對象。
Monitor必須和一個具體的對象相關(guān)聯(lián),但是由于它是一個靜態(tài)的類,所以不能使用它來定義對象,而且它的所有方法都是靜態(tài)的,不能使用對象來引用。下面代碼說明了使用Monitor鎖定一個對象的情形:
...... Queue oQueue=new Queue(); ...... Monitor.Enter(oQueue); ......//現(xiàn)在oQueue對象只能被當(dāng)前線程操縱了 Monitor.Exit(oQueue);//釋放鎖
如上所示,當(dāng)一個線程調(diào)用Monitor.Enter()方法鎖定一個對象時,這個對象就歸它所有了, 其它線程想要訪問這個對象,只有等待它使用Monitor.Exit()方法釋放鎖。為了保證線程最終都能釋放鎖,你可以把Monitor.Exit() 方法寫在try-catch-finally結(jié)構(gòu)中的finally代碼塊里。
對于任何一個被Monitor鎖定的對象,內(nèi)存中都保存著與它相關(guān)的一些信息:
其一是現(xiàn)在持有鎖的線程的引用;
其二是一個預(yù)備隊列,隊列中保存了已經(jīng)準(zhǔn)備好獲取鎖的線程;
其三是一個等待隊列,隊列中保存著當(dāng)前正在等待這個對象狀態(tài)改變的隊列的引用。
當(dāng)擁有對象鎖的線程準(zhǔn)備釋放鎖時,它使用Monitor.Pulse()方法通知等待隊列中的第一個線程,于是該線程被轉(zhuǎn)移到預(yù)備隊列中,當(dāng)對象鎖被釋放時,在預(yù)備隊列中的線程可以立即獲得對象鎖。
下面是一個展示如何使用lock關(guān)鍵字和Monitor類來實現(xiàn)線程的同步和通訊的例子,也是一個典型的生產(chǎn)者與消費者問題。
這個例程中,生產(chǎn)者線程和消費者線程是交替進(jìn)行的,生產(chǎn)者寫入一個數(shù),消費者立即讀取并且顯示(注釋中介紹了該程序的精要所在)。
用到的系統(tǒng)命名空間如下:
using System; using System.Threading;
首先,定義一個被操作的對象的類Cell,在這個類里,有兩個方法:ReadFromCell()和 WriteToCell。消費者線程將調(diào)用ReadFromCell()讀取cellContents的內(nèi)容并且顯示出來,生產(chǎn)者進(jìn)程將調(diào)用 WriteToCell()方法向cellContents寫入數(shù)據(jù)。
示例如下:
public class Cell { int cellContents; // Cell對象里邊的內(nèi)容 bool readerFlag = false; // 狀態(tài)標(biāo)志,為true時可以讀取,為false則正在寫入 public int ReadFromCell( ) { lock(this) // Lock關(guān)鍵字保證了什么,請大家看前面對lock的介紹 { if (!readerFlag)//如果現(xiàn)在不可讀取 { try { //等待WriteToCell方法中調(diào)用Monitor.Pulse()方法 Monitor.Wait(this); } catch (SynchronizationLockException e) { Console.WriteLine(e); } catch (ThreadInterruptedException e) { Console.WriteLine(e); } } Console.WriteLine("Consume: {0}",cellContents); readerFlag = false; //重置readerFlag標(biāo)志,表示消費行為已經(jīng)完成 Monitor.Pulse(this); //通知WriteToCell()方法(該方法在另外一個線程中執(zhí)行,等待中) } return cellContents; } public void WriteToCell(int n) { lock(this) { if (readerFlag) { try { Monitor.Wait(this); } catch (SynchronizationLockException e) { //當(dāng)同步方法(指Monitor類除Enter之外的方法)在非同步的代碼區(qū)被調(diào)用 Console.WriteLine(e); } catch (ThreadInterruptedException e) { //當(dāng)線程在等待狀態(tài)的時候中止 Console.WriteLine(e); } } cellContents = n; Console.WriteLine("Produce: {0}",cellContents); readerFlag = true; Monitor.Pulse(this); //通知另外一個線程中正在等待的ReadFromCell()方法 } } }
下面定義生產(chǎn)者類 CellProd 和消費者類 CellCons ,它們都只有一個方法ThreadRun(),以便在Main()函數(shù)中提供給線程的ThreadStart代理對象,作為線程的入口。
public class CellProd { Cell cell; // 被操作的Cell對象 int quantity = 1; // 生產(chǎn)者生產(chǎn)次數(shù),初始化為1 public CellProd(Cell box, int request) { //構(gòu)造函數(shù) cell = box; quantity = request; } public void ThreadRun( ) { for(int looper=1; looper<=quantity; looper++) cell.WriteToCell(looper); //生產(chǎn)者向操作對象寫入信息 } } public class CellCons { Cell cell; int quantity = 1; public CellCons(Cell box, int request) { //構(gòu)造函數(shù) cell = box; quantity = request; } public void ThreadRun( ) { int valReturned; for(int looper=1; looper<=quantity; looper++) valReturned=cell.ReadFromCell( );//消費者從操作對象中讀取信息 } }
然后在下面這個類MonitorSample的Main()函數(shù)中,我們要做的就是創(chuàng)建兩個線程分別作為生產(chǎn)者和消費者,使用CellProd.ThreadRun()方法和CellCons.ThreadRun()方法對同一個Cell對象進(jìn)行操作。
public class MonitorSample { public static void Main(String[] args) { int result = 0; //一個標(biāo)志位,如果是0表示程序沒有出錯,如果是1表明有錯誤發(fā)生 Cell cell = new Cell( ); //下面使用cell初始化CellProd和CellCons兩個類,生產(chǎn)和消費次數(shù)均為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)); //生產(chǎn)者線程和消費者線程都已經(jīng)被創(chuàng)建,但是沒有開始執(zhí)行 try { producer.Start( ); consumer.Start( ); producer.Join( ); consumer.Join( ); Console.ReadLine(); } catch (ThreadStateException e) { //當(dāng)線程因為所處狀態(tài)的原因而不能執(zhí)行被請求的操作 Console.WriteLine(e); result = 1; } catch (ThreadInterruptedException e) { //當(dāng)線程在等待狀態(tài)的時候中止 Console.WriteLine(e); result = 1; } //盡管Main()函數(shù)沒有返回值,但下面這條語句可以向父進(jìn)程返回執(zhí)行結(jié)果 Environment.ExitCode = result; } }
在上面的例程中,同步是通過等待Monitor.Pulse()來完成的。首先生產(chǎn)者生產(chǎn)了一個值,而 同一時刻消費者處于等待狀態(tài),直到收到生產(chǎn)者的“脈沖(Pulse)”通知它生產(chǎn)已經(jīng)完成,此后消費者進(jìn)入消費狀態(tài),而生產(chǎn)者開始等待消費者完成操作后將 調(diào)用Monitor.Pulese()發(fā)出的“脈沖”。
它的執(zhí)行結(jié)果很簡單:
Produce: 1
Consume: 1
Produce: 2
Consume: 2
Produce: 3
Consume: 3
...
...
Produce: 20
Consume: 20
事實上,這個簡單的例子已經(jīng)幫助我們解決了多線程應(yīng)用程序中可能出現(xiàn)的大問題,只要領(lǐng)悟了解決線程間沖突的基本方法,很容易把它應(yīng)用到比較復(fù)雜的程序中去。
希望本文所述對大家的C#程序設(shè)計有所幫助。
相關(guān)文章
- 本文章來給各位同學(xué)介紹關(guān)于C#單擊菜單欄或工具欄時通過反射打開窗體的方法,有需要了解的朋友可進(jìn)入?yún)⒖紖⒖肌?/div> 2015-05-05
淺談C#在網(wǎng)絡(luò)波動時防重復(fù)提交的方法
這篇文章主要介紹了淺談C#在網(wǎng)絡(luò)波動時防重復(fù)提交的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04C# 16進(jìn)制與字符串、字節(jié)數(shù)組之間的轉(zhuǎn)換
在串口通訊過程中,經(jīng)常要用到 16進(jìn)制與字符串、字節(jié)數(shù)組之間的轉(zhuǎn)換2009-05-05最新評論