[转]C#多线程学习(三) 生产者和消费者
前面說(shuō)過(guò),每個(gè)線程都有自己的資源,但是代碼區(qū)是共享的,即每個(gè)線程都可以執(zhí)行相同的函數(shù)。這可能帶來(lái)的問(wèn)題就是幾個(gè)線程同時(shí)執(zhí)行一個(gè)函數(shù),導(dǎo)致數(shù)據(jù)的混亂,產(chǎn)生不可預(yù)料的結(jié)果,因此我們必須避免這種情況的發(fā)生。
C#提供了一個(gè)關(guān)鍵字lock,它可以把一段代碼定義為互斥段(critical section),互斥段在一個(gè)時(shí)刻內(nèi)只允許一個(gè)線程進(jìn)入執(zhí)行,而其他線程必須等待。在C#中,關(guān)鍵字lock定義如下:
lock(expression) statement_block?
?
expression代表你希望跟蹤的對(duì)象,通常是對(duì)象引用。
??? 如果你想保護(hù)一個(gè)類的實(shí)例,一般地,你可以使用this;
??? 如果你想保護(hù)一個(gè)靜態(tài)變量(如互斥代碼段在一個(gè)靜態(tài)方法內(nèi)部),一般使用類名就可以了。
而statement_block就是互斥段的代碼,這段代碼在一個(gè)時(shí)刻內(nèi)只可能被一個(gè)線程執(zhí)行。
下面是一個(gè)使用lock關(guān)鍵字的典型例子,在注釋里說(shuō)明了lock關(guān)鍵字的用法和用途。
示例如下:
usingSystem.Threading;
namespaceThreadSimple
{
????internalclassAccount?
????{
????????intbalance;
????????Random?r?=newRandom();
????????
????????internalAccount(intinitial)?
????????{
????????????balance?=initial;
????????}?
????????internalintWithdraw(intamount)?
????????{
????????????if(balance?<0)
????????????{
????????????????//如果balance小于0則拋出異常
thrownewException("Negative?Balance");
????????????}
????????????//下面的代碼保證在當(dāng)前線程修改balance的值完成之前
????????????//不會(huì)有其他線程也執(zhí)行這段代碼來(lái)修改balance的值
????????????//因此,balance的值是不可能小于0?的
lock(this)
????????????{
????????????????Console.WriteLine("Current?Thread:"+Thread.CurrentThread.Name);
????????????????//如果沒(méi)有l(wèi)ock關(guān)鍵字的保護(hù),那么可能在執(zhí)行完if的條件判斷之后
????????????????//另外一個(gè)線程卻執(zhí)行了balance=balance-amount修改了balance的值
????????????????//而這個(gè)修改對(duì)這個(gè)線程是不可見(jiàn)的,所以可能導(dǎo)致這時(shí)if的條件已經(jīng)不成立了
????????????????//但是,這個(gè)線程卻繼續(xù)執(zhí)行balance=balance-amount,所以導(dǎo)致balance可能小于0
if(balance?>=amount)?
????????????????{
????????????????????Thread.Sleep(5);
????????????????????balance?=balance?-amount;
????????????????????returnamount;
????????????????}?
????????????????else
????????????????{
????????????????????return0;?//?transaction?rejected
}
????????????}
????????}
????????internalvoidDoTransactions()?
????????{
????????????for(inti?=0;?i?<100;?i++)?
????????????Withdraw(r.Next(-50,?100));
????????}
????}?
????internalclassTest?
????{
????????staticinternalThread[]?threads?=newThread[10];
????????publicstaticvoidMain()?
????????{
????????????Account?acc?=newAccount?(0);
????????????for(inti?=0;?i?<10;?i++)?
????????????{
????????????????Thread?t?=newThread(newThreadStart(acc.DoTransactions));
????????????????threads[i]?=t;
????????????}
????????????for(inti?=0;?i?<10;?i++)?
????????????????threads[i].Name=i.ToString();
????????????for(inti?=0;?i?<10;?i++)?
????????????????threads[i].Start();
????????????Console.ReadLine();
????????}
????}
}
?
Monitor 類鎖定一個(gè)對(duì)象
當(dāng)多線程公用一個(gè)對(duì)象時(shí),也會(huì)出現(xiàn)和公用代碼類似的問(wèn)題,這種問(wèn)題就不應(yīng)該使用lock關(guān)鍵字了,這里需要用到System.Threading中的一個(gè)類Monitor,我們可以稱之為監(jiān)視器,Monitor提供了使線程共享資源的方案。
Monitor類可以鎖定一個(gè)對(duì)象,一個(gè)線程只有得到這把鎖才可以對(duì)該對(duì)象進(jìn)行操作。對(duì)象鎖機(jī)制保證了在可能引起混亂的情況下一個(gè)時(shí)刻只有一個(gè)線程可以訪問(wèn)這個(gè)對(duì)象。
Monitor必須和一個(gè)具體的對(duì)象相關(guān)聯(lián),但是由于它是一個(gè)靜態(tài)的類,所以不能使用它來(lái)定義對(duì)象,而且它的所有方法都是靜態(tài)的,不能使用對(duì)象來(lái)引用。下面代碼說(shuō)明了使用Monitor鎖定一個(gè)對(duì)象的情形:
......
Queue oQueue=new Queue();
......
Monitor.Enter(oQueue);
......//現(xiàn)在oQueue對(duì)象只能被當(dāng)前線程操縱了
Monitor.Exit(oQueue);//釋放鎖?
?
如上所示,當(dāng)一個(gè)線程調(diào)用Monitor.Enter()方法鎖定一個(gè)對(duì)象時(shí),這個(gè)對(duì)象就歸它所有了,其它線程想要訪問(wèn)這個(gè)對(duì)象,只有等待它使用Monitor.Exit()方法釋放鎖。為了保證線程最終都能釋放鎖,你可以把Monitor.Exit()方法寫(xiě)在try-catch-finally結(jié)構(gòu)中的finally代碼塊里。
對(duì)于任何一個(gè)被Monitor鎖定的對(duì)象,內(nèi)存中都保存著與它相關(guān)的一些信息:
其一是現(xiàn)在持有鎖的線程的引用;
其二是一個(gè)預(yù)備隊(duì)列,隊(duì)列中保存了已經(jīng)準(zhǔn)備好獲取鎖的線程;
其三是一個(gè)等待隊(duì)列,隊(duì)列中保存著當(dāng)前正在等待這個(gè)對(duì)象狀態(tài)改變的隊(duì)列的引用。
當(dāng)擁有對(duì)象鎖的線程準(zhǔn)備釋放鎖時(shí),它使用Monitor.Pulse()方法通知等待隊(duì)列中的第一個(gè)線程,于是該線程被轉(zhuǎn)移到預(yù)備隊(duì)列中,當(dāng)對(duì)象鎖被釋放時(shí),在預(yù)備隊(duì)列中的線程可以立即獲得對(duì)象鎖。
下面是一個(gè)展示如何使用lock關(guān)鍵字和Monitor類來(lái)實(shí)現(xiàn)線程的同步和通訊的例子,也是一個(gè)典型的生產(chǎn)者與消費(fèi)者問(wèn)題。
這個(gè)例程中,生產(chǎn)者線程和消費(fèi)者線程是交替進(jìn)行的,生產(chǎn)者寫(xiě)入一個(gè)數(shù),消費(fèi)者立即讀取并且顯示(注釋中介紹了該程序的精要所在)。
用到的系統(tǒng)命名空間如下:
using System;
using System.Threading;
首先,定義一個(gè)被操作的對(duì)象的類Cell,在這個(gè)類里,有兩個(gè)方法:ReadFromCell()和WriteToCell。消費(fèi)者線程將調(diào)用ReadFromCell()讀取cellContents的內(nèi)容并且顯示出來(lái),生產(chǎn)者進(jìn)程將調(diào)用WriteToCell()方法向cellContents寫(xiě)入數(shù)據(jù)。
示例如下:
{
????????int?cellContents;?//Cell對(duì)象里邊的內(nèi)容
????????bool?readerFlag?=?false;?//狀態(tài)標(biāo)志,為true時(shí)可以讀取,為false則正在寫(xiě)入
????????public?int?ReadFromCell(?)
????????{
????????????lock(this)?//Lock關(guān)鍵字保證了什么,請(qǐng)大家看前面對(duì)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)志,表示消費(fèi)行為已經(jīng)完成
????????????????Monitor.Pulse(this);?
????????????????//通知WriteToCell()方法(該方法在另外一個(gè)線程中執(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)的時(shí)候中止?
????????????????????????Console.WriteLine(e);
????????????????????}
????????????????}
????????????????cellContents?=?n;
????????????????Console.WriteLine("Produce:?{0}",cellContents);
????????????????readerFlag?=?true;?
????????????????Monitor.Pulse(this);?
????????????????//通知另外一個(gè)線程中正在等待的ReadFromCell()方法
????????????}
????????}
}
下面定義生產(chǎn)者類 CellProd 和消費(fèi)者類 CellCons ,它們都只有一個(gè)方法ThreadRun(),以便在Main()函數(shù)中提供給線程的ThreadStart代理對(duì)象,作為線程的入口。
{
????Cell?cell;?//被操作的Cell對(duì)象
????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)者向操作對(duì)象寫(xiě)入信息
????}
}
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(?);//消費(fèi)者從操作對(duì)象中讀取信息
????}
}?
然后在下面這個(gè)類MonitorSample的Main()函數(shù)中,我們要做的就是創(chuàng)建兩個(gè)線程分別作為生產(chǎn)者和消費(fèi)者,使用CellProd.ThreadRun()方法和CellCons.ThreadRun()方法對(duì)同一個(gè)Cell對(duì)象進(jìn)行操作。
{
????public?static?void?Main(String[]?args)
????{
????????int?result?=?0;?//一個(gè)標(biāo)志位,如果是0表示程序沒(méi)有出錯(cuò),如果是1表明有錯(cuò)誤發(fā)生
????????Cell?cell?=?new?Cell(?);?
????????//下面使用cell初始化CellProd和CellCons兩個(gè)類,生產(chǎn)和消費(fèi)次數(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)者線程和消費(fèi)者線程都已經(jīng)被創(chuàng)建,但是沒(méi)有開(kāi)始執(zhí)行?
????????try
????????{
????producer.Start(?);
????consumer.Start(?);?
????producer.Join(?);?
????consumer.Join(?);
????Console.ReadLine();
????????}
????????catch?(ThreadStateException?e)
????????{
????//當(dāng)線程因?yàn)樗帬顟B(tài)的原因而不能執(zhí)行被請(qǐng)求的操作
????Console.WriteLine(e);?
????result?=?1;?
????????}
????????catch?(ThreadInterruptedException?e)
????????{
????//當(dāng)線程在等待狀態(tài)的時(shí)候中止
????Console.WriteLine(e);?
????result?=?1;?
????????}
????????//盡管Main()函數(shù)沒(méi)有返回值,但下面這條語(yǔ)句可以向父進(jìn)程返回執(zhí)行結(jié)果
????????Environment.ExitCode?=?result;
????}
}
在上面的例程中,同步是通過(guò)等待Monitor.Pulse()來(lái)完成的。首先生產(chǎn)者生產(chǎn)了一個(gè)值,而同一時(shí)刻消費(fèi)者處于等待狀態(tài),直到收到生產(chǎn)者的“脈沖(Pulse)”通知它生產(chǎn)已經(jīng)完成,此后消費(fèi)者進(jìn)入消費(fèi)狀態(tài),而生產(chǎn)者開(kāi)始等待消費(fèi)者完成操作后將調(diào)用Monitor.Pulese()發(fā)出的“脈沖”。
它的執(zhí)行結(jié)果很簡(jiǎn)單:
Produce: 1
Consume: 1
Produce: 2
Consume: 2
Produce: 3
Consume: 3
...
...
Produce: 20
Consume: 20?
?
事實(shí)上,這個(gè)簡(jiǎn)單的例子已經(jīng)幫助我們解決了多線程應(yīng)用程序中可能出現(xiàn)的大問(wèn)題,只要領(lǐng)悟了解決線程間沖突的基本方法,很容易把它應(yīng)用到比較復(fù)雜的程序中去。
文章來(lái)源:http://www.cnblogs.com/xugang/archive/2008/03/23/1118594.html
轉(zhuǎn)載于:https://www.cnblogs.com/yuanyl/p/3234312.html
總結(jié)
以上是生活随笔為你收集整理的[转]C#多线程学习(三) 生产者和消费者的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Android借助Application
- 下一篇: BFS+状态压缩 hdu-1885-Ke