lock(x)
{
DoSomething();
}
這等效于:
System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
DoSomething();
}
finally
{
System.Threading.Monitor.Exit(obj);
}
臨界區(qū)&Lock
一個(gè)機(jī)會(huì),,索性把線程同步的問(wèn)題在C#里面的東西都粗略看了下,。
第一印象,,C#關(guān)于線程同步的東西好多,,保持了C#一貫的大雜燴和四不象風(fēng)格(Java/Delphi)。臨界區(qū)跟Java差不多只不過(guò)關(guān)鍵字用lock替代了synchronized,,然后又用Moniter的Wait/Pulse取代了Object的Wait/Notify,,另外又搞出來(lái)幾個(gè)Event……讓人甚是不明了。不管那么多,,一個(gè)一個(gè)來(lái)吧,。
臨界區(qū)(Critical Section)
是一段在同一時(shí)候只被一個(gè)線程進(jìn)入/執(zhí)行的代碼。為啥要有這個(gè)東西,?
- 是因?yàn)檫@段代碼訪問(wèn)了“臨界資源”,,而這種資源只能同時(shí)被互斥地訪問(wèn)。舉個(gè)例子來(lái)說(shuō),,你的銀行賬戶就是一個(gè)互斥資源,,一個(gè)銀行系統(tǒng)里面改變余額(存取)的操作代碼就必須用在臨界區(qū)內(nèi),。如果你的賬戶余額是$100,000(如果是真的,,那么你就不用再往下看了,,還是睡覺(jué)去吧),,假設(shè)有兩個(gè)人同時(shí)給你匯款$50,000,。有兩個(gè)線程分別執(zhí)行這兩筆匯款業(yè)務(wù),線程A在獲取了你的賬戶余額后,,在它把新余額($150000)儲(chǔ)存回?cái)?shù)據(jù)庫(kù)以前,操作系統(tǒng)把這個(gè)線程暫停轉(zhuǎn)而把CPU的時(shí)間片分給另一個(gè)線程(是的,,這太巧了),;那么線程B此時(shí)取出的賬戶余額仍然是$10000,隨后線程B幸運(yùn)的得到的CPU時(shí)間把$50000存入你的賬戶,,那么余額變成$150000,。而此后某個(gè)時(shí)候,,線程A再次得以執(zhí)行,,它也把“新”余額$150000更新到系統(tǒng)……于是你的$50000就這么憑空消失了,。(此段省去常見(jiàn)到一個(gè)示例圖,,請(qǐng)自行想象)
- 是因?yàn)镺S的多任務(wù)調(diào)度,其實(shí)在原因一里面已經(jīng)提到,。如果OS不支持多任務(wù)調(diào)度,,那么線程A/線程B執(zhí)行更新余額的操作總是一個(gè)接一個(gè)進(jìn)行,那么完全不會(huì)有上面的問(wèn)題了,。在多線程的世界里,,你必須隨時(shí)做好你的代碼執(zhí)行過(guò)程隨時(shí)失去控制的準(zhǔn)備;你需要好好考慮當(dāng)代碼重新執(zhí)行的時(shí)候,,是否可以繼續(xù)正確的執(zhí)行,。一句話,你的程序段在多線程的世界里,,你所寫(xiě)的方法并不是“原子性”的操作,。
Lock關(guān)鍵字
C#提供lock關(guān)鍵字實(shí)現(xiàn)臨界區(qū),MSDN里給出的用法:
Object thisLock = new Object();
lock (thisLock)
{
// Critical code section
}
lock實(shí)現(xiàn)臨界區(qū)是通過(guò)“對(duì)象鎖”的方式,,注意是“對(duì)象”,,所以你只能鎖定一個(gè)引用類型而不能鎖定一個(gè)值類型。第一個(gè)執(zhí)行該代碼的線程,,成功獲取對(duì)這個(gè)對(duì)象的鎖定,,進(jìn)而進(jìn)入臨界區(qū)執(zhí)行代碼。而其它線程在進(jìn)入臨界區(qū)前也會(huì)請(qǐng)求該鎖,,如果此時(shí)第一個(gè)線程沒(méi)有退出臨界區(qū),,對(duì)該對(duì)象的鎖定并沒(méi)有解除,那么當(dāng)前線程會(huì)被阻塞,,等待對(duì)象被釋放,。
既然如此,在使用lock時(shí),要注意不同線程是否使用同一個(gè)“鎖”作為lock的對(duì)象?,F(xiàn)在回頭來(lái)看MSDN的這段代碼似乎很容易讓人誤解,,容易讓人聯(lián)想到這段代碼是在某個(gè)方法中存在,以為thisLock是一個(gè)局部變量,,而局部變量的生命周期是在這個(gè)方法內(nèi)部,,所以當(dāng)不同線程調(diào)用這個(gè)方法的時(shí)候,他們分別請(qǐng)求了不同的局部變量作為鎖,,那么他們都可以分別進(jìn)入臨界區(qū)執(zhí)行代碼,。因此在MSDN隨后真正的示例中,thisLock實(shí)際上是一個(gè) private的類成員變量:
using System;
using System.Threading;
class Account
{
private Object thisLock = new Object();
int balance;
Random r = new Random();
public Account(int initial)
{
balance = initial;
}
int Withdraw(int amount)
{
// This condition will never be true unless the lock statement
// is commented out:
if (balance < 0)
{
throw new Exception("Negative Balance");
}
// Comment out the next line to see the effect of leaving out
// the lock keyword:
lock(thisLock)
{
if (balance >= amount)
{
Console.WriteLine("Balance before Withdrawal : " + balance);
Console.WriteLine("Amount to Withdraw : -" + amount);
balance = balance - amount;
Console.WriteLine("Balance after Withdrawal : " + balance);
return amount;
}
else
{
return 0; // transaction rejected
}
}
}
public void DoTransactions()
{
for (int i = 0; i < 100; i++)
{
Withdraw(r.Next(1, 100));
}
}
}
class Test
{
static void Main()
{
Thread[] threads = new Thread[10];
Account acc = new Account(1000);
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].Start();
}
}
}
這個(gè)例子中,,Account對(duì)象只有一個(gè),,所以臨界區(qū)所請(qǐng)求的“鎖”是唯一的,因此用類的成員變量是可以實(shí)現(xiàn)互斥意圖的,,其實(shí)用大家通常喜歡的 lock(this)也未嘗不可,,也即請(qǐng)求這個(gè)Account實(shí)例本身作為鎖。但是如果在某種情況你的類實(shí)例并不唯一或者一個(gè)類的幾個(gè)方法之間都必須要互斥,,那么就要小心了,。必須牢記一點(diǎn),所有因?yàn)橥换コ赓Y源而需要互斥的操作,,必須請(qǐng)求“同一把鎖”才有效,。
假設(shè)這個(gè)Account類并不只有一個(gè)Withdraw方法修改balance,而是用Withdraw()來(lái)特定執(zhí)行取款操作,,另有一個(gè) Deposit()方法專門執(zhí)行存款操作,。很顯然這兩個(gè)方法必須是互斥執(zhí)行的,所以這兩個(gè)方法中所用到的鎖也必須一致,;不能一個(gè)用thisLock,,另一個(gè)重新用一個(gè)private Object thisLock1 = new Object()。再進(jìn)一步,,其實(shí)這個(gè)操作場(chǎng)景下各個(gè)互斥區(qū)存在的目的是因?yàn)橛小癇alance”這個(gè)互斥資源,,所有有關(guān)Balance的地方應(yīng)該都是互斥的(如果你不介意讀取操作讀到的是臟數(shù)據(jù)的話,當(dāng)然也可以不用),。
題外話:
這么看來(lái)其實(shí)用 Balance本身作為鎖也許更為符合“邏輯”,,lock住需要互斥的資源本身不是更好理解么?不過(guò)這里Balance是一個(gè)值類型,,你并不能直接對(duì)它 lock(你可能需要用到volatile關(guān)鍵字,,它能在單CPU的情況下確保只有一個(gè)線程修改一個(gè)變量)。
Lock使用的建議
關(guān)于使用Lock微軟給出的一些建議,。你能夠在MSDN上找到這么一段話:
通常,,應(yīng)避免鎖定 public 類型,,否則實(shí)例將超出代碼的控制范圍。常見(jiàn)的結(jié)構(gòu) lock (this),、lock (typeof (MyType)) 和 lock ("myLock") 違反此準(zhǔn)則:
1.如果實(shí)例可以被公共訪問(wèn),,將出現(xiàn) lock (this) 問(wèn)題。
2.如果 MyType 可以被公共訪問(wèn),,將出現(xiàn) lock (typeof (MyType)) 問(wèn)題,。
3.由于進(jìn)程中使用同一字符串的任何其他代碼將共享同一個(gè)鎖,所以出現(xiàn) lock("myLock") 問(wèn)題,。
4.最佳做法是定義 private 對(duì)象來(lái)鎖定, 或 private static 對(duì)象變量來(lái)保護(hù)所有實(shí)例所共有的數(shù)據(jù),。
lock(this)的問(wèn)題我是這么理解:
- 處于某種原因Account在整個(gè)程序空間內(nèi)不是唯一,那么不同Account實(shí)例的相應(yīng)方法就不可能互斥,,因?yàn)樗麄冋?qǐng)求的是不同Accout實(shí)例內(nèi)部的不同的鎖,。這時(shí)候微軟示例中的private Object thisLock仍然也避免不了這個(gè)問(wèn)題,而需要使用private static Object thisLock來(lái)解決問(wèn)題,,因?yàn)閟tatic變量是所有類實(shí)例共享的,。
- 猜想就算Account只有一個(gè)實(shí)例,但是如果在程序內(nèi)部被多個(gè)處理不同任務(wù)的線程訪問(wèn),,那么Account實(shí)例可能會(huì)被某段代碼直接作為鎖鎖定,;這相當(dāng)于你自己鎖定了自己,,而別人在不告訴你的情況下也可以能鎖定你,。這些情況都是你在寫(xiě)Account這個(gè)類的時(shí)候并沒(méi)有辦法作出預(yù)測(cè)的,所以你的 Withdraw代碼可能被掛起,,在多線程的復(fù)雜情況下也容易造成死鎖,。不管怎樣,你寫(xiě)這段代碼的時(shí)候肯定不會(huì)期待外部的代碼跟你使用了同一把鎖吧,?這樣很危險(xiǎn),。另外,從面向?qū)ο髞?lái)說(shuō),,這等于把方法內(nèi)部的東西隱式的暴露出去,。為了實(shí)現(xiàn)互斥,專門建立不依賴系this的代碼機(jī)制總是好的,;thisLock,,專事專用,是個(gè)好習(xí)慣,。
MyType的問(wèn)題跟lock(this)差不多理解,,不過(guò)比lock(this)更嚴(yán)重。因?yàn)長(zhǎng)ock(typeof(MyType))鎖定住的對(duì)象范圍更為廣泛,,由于一個(gè)類的所有實(shí)例都只有一個(gè)類對(duì)象(就是擁有Static成員的那個(gè)對(duì)象實(shí)例),,鎖定它就鎖定了該對(duì)象的所有實(shí)例。同時(shí) lock(typeof(MyType))是個(gè)很緩慢的過(guò)程,并且類中的其他線程,、甚至在同一個(gè)應(yīng)用程序域中運(yùn)行的其他程序都可以訪問(wèn)該類型對(duì)象,,因此,它們都有可能鎖定類對(duì)象,,完全阻止你代碼的執(zhí)行,,導(dǎo)致你自己代碼的掛起或者死鎖。
至于lock("myLock"),,是因?yàn)樵?NET中字符串會(huì)被暫時(shí)存放,。如果兩個(gè)變量的字符串內(nèi)容相同的話,.NET會(huì)把暫存的字符串對(duì)象分配給該變量,。所以如果有兩個(gè)地方都在使用lock(“my lock”)的話,,它們實(shí)際鎖住的是同一個(gè)對(duì)象。
.NET集合類對(duì)lock的支持
在多線程環(huán)境中,,常會(huì)碰到的互斥資源應(yīng)該就是一些容器/集合,。因此.NET在一些集合類中(比如 ArrayList,HashTable,Queue,,Stack,,包括新增的支持泛型的List)已經(jīng)提供了一個(gè)供lock使用的對(duì)象 SyncRoot。
在.Net1.1中大多數(shù)集合類的SyncRoot屬性只有一行代碼:return this,,這樣和lock(集合的當(dāng)前實(shí)例)是一樣的,。不過(guò)ArrayList中的SyncRoot有所不同(這個(gè)并不是我反編譯的,我并沒(méi)有驗(yàn)證這個(gè)說(shuō)法):
get
{
if(this._syncRoot==null)
{
Interlocked.CompareExchange(refthis._syncRoot,newobject(),null);
}
returnthis._syncRoot;
}
題外話:
上面反編譯的 ArrayList的代碼,,引出了個(gè)Interlocked類,,即互鎖操作,用以對(duì)某個(gè)內(nèi)存位置執(zhí)行的簡(jiǎn)單原子操作,。舉例來(lái)說(shuō)在大多數(shù)計(jì)算機(jī)上,,增加變量操作不是一個(gè)原子操作,需要執(zhí)行下列步驟:
- 將實(shí)例變量中的值加載到寄存器中,。
- 增加或減少該值,。
- 在實(shí)例變量中存儲(chǔ)該值。
線程可能會(huì)在執(zhí)行完前兩個(gè)步驟后被奪走CPU時(shí)間,,然后由另一個(gè)線程執(zhí)行所有三個(gè)步驟,。當(dāng)?shù)谝粋€(gè)線程重新再開(kāi)始執(zhí)行時(shí),它改寫(xiě)實(shí)例變量中的值,,造成第二個(gè)線程執(zhí)行增減操作的結(jié)果丟失,。這根我們上面提到的銀行賬戶余額的例子是一個(gè)道理,不過(guò)是更微觀上的體現(xiàn),。我們使用該類提供了的 Increment和Decrement方法就可以避免這個(gè)問(wèn)題,。
另外,,Interlocked類上提供了其它一些能保證對(duì)相關(guān)變量的操作是原子性的方法。如Exchange()可以保證指定變量的值交換操作的原子性,,Read()保證在32位操作系統(tǒng)中對(duì)64位變量的原子讀取,。而這里使用的 CompareExchange方法組合了兩個(gè)操作:保證了比較和交換操作按原子操作執(zhí)行。此例中CompareExchange方法將當(dāng)前 syncRoot和null做比較,,如果相等,,就用new object()替換SyncRoot。
在現(xiàn)代處理器中,,Interlocked 類的方法經(jīng)??梢杂蓡蝹€(gè)指令來(lái)實(shí)現(xiàn),因此它們的執(zhí)行性能非常高,。雖然Interlocked沒(méi)有直接提供鎖定或者發(fā)送信號(hào)的能力,,但是你可以用它編寫(xiě)鎖和信號(hào),從而編寫(xiě)出高效的非阻止并發(fā)的應(yīng)用程序,。但是這需要復(fù)雜的低級(jí)別編程能力,,因此大多數(shù)情況下使用lock或其它簡(jiǎn)單鎖是更好的選擇。
看到這里是不是已經(jīng)想給微軟一耳光了,?一邊教導(dǎo)大家不要用lock(this),,一邊竟然在基礎(chǔ)類庫(kù)中大量使用……呵呵,我只能說(shuō)據(jù)傳從.Net2.0開(kāi)始SyncRoot已經(jīng)是會(huì)返回一個(gè)單獨(dú)的類了,,想來(lái)大約應(yīng)該跟ArrayList那種實(shí)現(xiàn)差不多,,有興趣的可以反編譯驗(yàn)證下。
這里想說(shuō),,代碼是自己的寫(xiě)的,,最好減少自己代碼對(duì)外部環(huán)境的依賴,,事實(shí)證明即便是.Net基礎(chǔ)庫(kù)也不是那么可靠,。自己能想到的問(wèn)題,最好自己寫(xiě)代碼去處理,,需要鎖就自己聲明一個(gè)鎖,;不再需要一個(gè)資源那么自己代碼去Dispose掉(如果是實(shí)現(xiàn)IDisposable接口的)……不要想著什么東西系統(tǒng)已經(jīng)幫你做了。你永遠(yuǎn)無(wú)法保證你的類將會(huì)在什么環(huán)境下被使用,,你也無(wú)法預(yù)見(jiàn)到下一版的Framework是否偷偷改變了實(shí)現(xiàn),。當(dāng)你代碼莫名其妙不 Work的時(shí)候,你是很難找出由這些問(wèn)題引發(fā)的麻煩,。只有你代碼足夠的獨(dú)立(這里沒(méi)有探討代碼耦合度的問(wèn)題),,才能保證它足夠的健壯;別人代碼的修改(哪怕是你看來(lái)“不當(dāng)”的修改),,造成你的Code無(wú)法工作不是總有些可笑么(我還想說(shuō)“蒼蠅不叮無(wú)縫的蛋”“不要因?yàn)閯e人的錯(cuò)誤連累自己”),?
一些集合類中還有一個(gè)方法是和同步相關(guān)的:Synchronized,,該方法返回一個(gè)集合的內(nèi)部類,該類是線程安全的,,因?yàn)樗拇蟛糠址椒ǘ加?lock來(lái)進(jìn)行了同步處理(你會(huì)不會(huì)想那么SyncRoot顯得多余,?別急。),。比如,,Add方法會(huì)類似于:
public override void Add(objectkey,objectvalue)
{
lock(this._table.SyncRoot)
{
this._table.Add(key,value);
}
}
不過(guò)即便是這個(gè)Synchronized集合,在對(duì)它進(jìn)行遍歷時(shí),,仍然不是一個(gè)線程安全的過(guò)程,。當(dāng)你遍歷它時(shí),其他線程仍可以修改該它(Add,、Remove),,可能會(huì)導(dǎo)致諸如下標(biāo)越界之類的異常;就算不出錯(cuò),,你也可能讀到臟數(shù)據(jù),。若要在遍歷過(guò)程中保證線程安全,還必須在整個(gè)遍歷過(guò)程中鎖定集合,,我想這才是SynRoot存在的目的吧:
Queue myCollection = newQueue();
lock(myCollection.SyncRoot)
{
foreach(ObjectiteminmyCollection)
{
//Insert your code here.
}
}
提供SynRoot是為了把這個(gè)已經(jīng)“線程安全”的集合內(nèi)部所使用的“鎖”暴露給你,,讓你和它內(nèi)部的操作使用同一把鎖,這樣才能保證在遍歷過(guò)程互斥掉其它操作,,保證你在遍歷的同時(shí)沒(méi)有可以修改,。另一個(gè)可以替代的方法,是使用集合上提供的靜態(tài)ReadOnly()方法,,來(lái)返回一個(gè)只讀的集合,,并對(duì)它進(jìn)行遍歷,這個(gè)返回的只讀集合是線程安全的,。
到這里似乎關(guān)于集合同步的方法似乎已經(jīng)比較清楚了,,不過(guò)如果你是一個(gè)很迷信MS基礎(chǔ)類庫(kù)的人,那么這次恐怕又會(huì)失望了,。微軟決定所有從那些自 Framwork 3.0以來(lái)加入的支持泛型的集合中,,如List,取消掉創(chuàng)建同步包裝器的能力,,也就是它們不再有Synchronized,,IsSynchronized 也總會(huì)返回false;而ReadOnly這個(gè)靜態(tài)方法也變?yōu)槊麨锳sReadOnly的實(shí)例方法,。作為替代,,MS建議你仍然使用lock關(guān)鍵字來(lái)鎖定整個(gè)集合。
至于List之類的泛型集合SyncRoot是怎樣實(shí)現(xiàn)的,,MSDN是這樣描述的“在 List<(Of <(T>)>) 的默認(rèn)實(shí)現(xiàn)中,,此屬性始終返回當(dāng)前實(shí)例,。”,,趕緊去吐血吧,!
自己的SyncRoot
還是上面提過(guò)的老話,靠自己,,以不變應(yīng)萬(wàn)變:
public class MySynchronizedList
{
private readonly object syncRoot = new object();
private readonly List<intlist = new List<int>();
public object SyncRoot
{
get{return this.syncRoot;}
}
public void Add(int i)
{
lock(syncRoot)
{
list.Add(i);
}
}
//...
}
自已寫(xiě)一個(gè)類,,用自己的syncRoot封裝一個(gè)線程安全的容器。
臨界區(qū)&Monitor
監(jiān)視器(Monitor)的概念
可以在MSDN(http://msdn.microsoft.com/zh-cn/library/ms173179(VS.80).aspx)上找到下面一段話:
與lock關(guān)鍵字類似,,監(jiān)視器防止多個(gè)線程同時(shí)執(zhí)行代碼塊,。Enter方法允許一個(gè)且僅一個(gè)線程繼續(xù)執(zhí)行后面的語(yǔ)句;其他所有線程都將被阻止,,直到執(zhí)行語(yǔ)句的線程調(diào)用Exit,。這與使用lock關(guān)鍵字一樣。事實(shí)上,,lock 關(guān)鍵字就是用Monitor 類來(lái)實(shí)現(xiàn)的,。例如:
lock(x)
{
DoSomething();
}
這等效于:
System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
DoSomething();
}
finally
{
System.Threading.Monitor.Exit(obj);
}
使用 lock 關(guān)鍵字通常比直接使用 Monitor 類更可取,一方面是因?yàn)?lock 更簡(jiǎn)潔,,另一方面是因?yàn)?lock 確保了即使受保護(hù)的代碼引發(fā)異常,,也可以釋放基礎(chǔ)監(jiān)視器。這是通過(guò) finally 關(guān)鍵字來(lái)實(shí)現(xiàn)的,,無(wú)論是否引發(fā)異常它都執(zhí)行關(guān)聯(lián)的代碼塊,。
這里微軟已經(jīng)說(shuō)得很清楚了,Lock就是用Monitor實(shí)現(xiàn)的,,兩者都是C#中對(duì)臨界區(qū)功能的實(shí)現(xiàn),。用ILDASM打開(kāi)含有以下代碼的exe 或者dll也可以證實(shí)這一點(diǎn)(我并沒(méi)有自己證實(shí)):
lock (lockobject)
{
int i = 5;
}
反編譯后的的IL代碼為:
IL_0045: call void [mscorlib]System.Threading.Monitor::Enter(object)
IL_004a: nop
.try
{
IL_004b: nop
IL_004c: ldc.i4.5
IL_004d: stloc.1
IL_004e: nop
IL_004f: leave.s IL_0059
} // end .try
finally
{
IL_0051: ldloc.3
IL_0052: call void [mscorlib]System.Threading.Monitor::Exit(object)
IL_0057: nop
IL_0058: endfinally
} // end handler
Monitor中和lock等效的方法
Monitor是一個(gè)靜態(tài)類,因此不能被實(shí)例化,,只能直接調(diào)用Monitor上的各種方法來(lái)完成與lock相同的功能:
- Enter(object)/TryEnter(object)/TryEnter(object, int32)/TryEnter(object, timespan):用來(lái)獲取對(duì)象鎖(Lock中已經(jīng)提到過(guò),,這里再?gòu)?qiáng)調(diào)一次,是對(duì)象類型而不能是值類型),,標(biāo)記臨界區(qū)的開(kāi)始,。與Enter不同,TryEnter永遠(yuǎn)不會(huì)阻塞代碼,,當(dāng)無(wú)法獲取對(duì)象鎖時(shí)它會(huì)返回False,并且調(diào)用者不進(jìn)入臨界區(qū),。TryEnter還有兩種重載,,可以定義一個(gè)時(shí)間段,在該時(shí)間段內(nèi)一直嘗試獲得對(duì)象鎖,,超時(shí)則返回False,。
- Exit(object):沒(méi)啥好說(shuō)的,,釋放對(duì)象鎖、退出臨界區(qū),。只是一定記得在try的finally塊里調(diào)用,,否則一但由于異常造成Exit無(wú)法執(zhí)行,對(duì)象鎖得不到釋放,,就會(huì)造成死鎖,。此外,調(diào)用Exit的線程必須擁有 object 參數(shù)上的鎖,,否則會(huì)引發(fā)SynchronizationLockException異常,。在調(diào)用線程獲取指定對(duì)象上的鎖后,可以重復(fù)對(duì)該對(duì)象進(jìn)行了相同次數(shù)的 Exit 和 Enter 調(diào)用,;如果調(diào)用 Exit 與調(diào)用 Enter 的次數(shù)不匹配,,那么該鎖不會(huì)被正確釋放。
上篇中提到的有關(guān)lock的所有使用方法和建議,,都適用于它們,。
比lock更“高級(jí)”的Monitor
到此為止,所有見(jiàn)到的還是我們?cè)趌ock中熟悉的東西,,再看Monitor的其它方法之前,,我們來(lái)看看那老掉牙的“生產(chǎn)者和消費(fèi)者”場(chǎng)景。試想消費(fèi)者和生產(chǎn)者是兩個(gè)獨(dú)立的線程,,同時(shí)訪問(wèn)一個(gè)容器:
- 很顯然這個(gè)容器是一個(gè)臨界資源(你不會(huì)問(wèn)我為什么是顯然吧,?),同時(shí)只允許一個(gè)線程訪問(wèn),。
- 生產(chǎn)者往容器里存放生產(chǎn)好的資源,;消費(fèi)者消費(fèi)掉容器里的資源。
粗看這個(gè)場(chǎng)景并沒(méi)有什么特殊的問(wèn)題,,只要在兩個(gè)線程中分別調(diào)用兩個(gè)方法,,這兩個(gè)方法內(nèi)部都用同一把鎖進(jìn)入臨界區(qū)訪問(wèn)容器即可??墒菃?wèn)題在于:
- 消費(fèi)者鎖定容器,,進(jìn)入臨界區(qū)后可能發(fā)現(xiàn)容器是空的。它可以退出臨界區(qū),,然后下次再盲目地進(jìn)入碰碰運(yùn)氣,;如果不退出,那么讓生產(chǎn)者永遠(yuǎn)無(wú)法進(jìn)入臨界區(qū),,往容器里放入資源供消費(fèi)者消費(fèi),,從而造成死鎖。
- 而生產(chǎn)者也可能進(jìn)入臨界區(qū)后,,卻發(fā)現(xiàn)容器是滿的,。結(jié)果一樣,,直接退出等下次來(lái)碰運(yùn)氣;或者不退出造成死鎖,。
兩者選擇直接退出不會(huì)引發(fā)什么問(wèn)題,,無(wú)非就是可能多次無(wú)功而返。這么做,,你的程序邏輯總是有機(jī)會(huì)得到正確執(zhí)行的,,但是效率很低,因?yàn)檫@樣的機(jī)制本身是不可控的,,業(yè)務(wù)邏輯是否得以成功執(zhí)行完全是隨機(jī)的,。
所以我們需要更有效、更“優(yōu)雅”的方式:
- 消費(fèi)者在進(jìn)入臨界區(qū)發(fā)現(xiàn)容器為空后,,立即釋放鎖并把自己阻塞,,等待生產(chǎn)者通知,不再做無(wú)謂的嘗試,;如果順利消費(fèi)資源完畢后,,主動(dòng)通知生產(chǎn)者可以進(jìn)行生產(chǎn)了,隨后仍然阻塞自己等待生產(chǎn)者通知,。
- 生產(chǎn)者如果發(fā)現(xiàn)容器是滿的,,那么立即釋放鎖并阻塞自己,等待消費(fèi)者在消費(fèi)完成后喚醒,;在生產(chǎn)完畢后,,主動(dòng)給消費(fèi)者發(fā)出通知,隨后也仍然阻塞自己,,等待消費(fèi)者告訴自己容器已經(jīng)空了,。
在按這個(gè)思路寫(xiě)出Sample Code前,我們來(lái)看Monitor上需要用的其它重要方法:
- Wait(Object)/Wait(Object, Int32)/Wait(Object, TimeSpan)/Wait(Object, Int32, Boolean)/Wait(Object, TimeSpan, Boolean): 釋放對(duì)象上的鎖并阻塞當(dāng)前線程,,直到它重新獲取該鎖,。
- 這里的阻塞是指當(dāng)前線程進(jìn)入“WaitSleepJoin”狀態(tài),此時(shí)CPU不再會(huì)分配給這種狀態(tài)的線程CPU時(shí)間片,,這其實(shí)跟在線程上調(diào)用 Sleep()時(shí)的狀態(tài)一樣,。這時(shí),線程不會(huì)參與對(duì)該鎖的分配爭(zhēng)奪,。
- 要打破這種狀態(tài),,需要其它擁有該對(duì)象鎖的線程,調(diào)用下面要講到的Pulse()來(lái)喚醒,。不過(guò)這與,,Sleep()不同,只有那些因?yàn)樵搶?duì)象鎖阻塞的線程才會(huì)被喚醒,。此時(shí),,線程重新進(jìn)入“Running”狀態(tài),參與對(duì)對(duì)象鎖的爭(zhēng)奪,。
- 強(qiáng)調(diào)一下,,Wait()其實(shí)起到了Exit()的作用,也就是釋放當(dāng)前所獲得的對(duì)象鎖,。只不過(guò)Wait()同時(shí)又阻塞了自己,。
- 我們還看到Wait()的幾個(gè)重載方法。其中第2,、3個(gè)方法給Wait加上了一個(gè)時(shí)間,,如果超時(shí)Wait會(huì)返回不再阻塞,并且可以根據(jù)Wait 方法的返回值,,以確定它是否已在超時(shí)前重新獲取鎖,。在這種情況下,其實(shí)線程并不需要等待其它線程Pulse()喚醒,,相當(dāng)于Sleep一定時(shí)間后醒來(lái),。第 4、5個(gè)方法在第2,、3個(gè)方法的基礎(chǔ)上加上exitContent參數(shù),,我們暫時(shí)不去管它,你可以詳細(xì)參見(jiàn)這里:http://msdn.microsoft.com/zh-cn/library/79fkfcw1(VS.85).aspx,。
- Pulse(object):向阻塞線程隊(duì)列(由于該object而轉(zhuǎn)入WaitSleepJoin狀態(tài)的所有線程,,也就是那些執(zhí)行了Wait(object)的線程,存放的隊(duì)列)中第一個(gè)線程發(fā)信號(hào),,該信號(hào)通知鎖定對(duì)象的狀態(tài)已更改,,并且鎖的所有者準(zhǔn)備釋放該鎖。收到信號(hào)的阻塞線程進(jìn)入就緒隊(duì)列中(那些處于Running狀態(tài)的線程,,可以被CPU調(diào)用運(yùn)行的線程在這個(gè)隊(duì)列里),,以便它有機(jī)會(huì)接收對(duì)象鎖。注意,,接受到信號(hào)的線程只會(huì)從阻塞中被喚醒,,并不一定會(huì)獲得對(duì)象鎖。
- PulseAll(object):與Pulse()不同,,阻塞隊(duì)列中的所有線程都會(huì)收到信號(hào),,并被喚醒轉(zhuǎn)入 Running狀態(tài),即進(jìn)入就緒隊(duì)列中,。至于它們誰(shuí)會(huì)幸運(yùn)的獲得對(duì)象鎖,,那就要看CPU了。
- 注意:以上所有方法都只能在臨界區(qū)內(nèi)被調(diào)用,換句話說(shuō),,只有對(duì)象鎖的獲得者能夠正確調(diào)用它們,,否則會(huì)引發(fā) SynchronizationLockException異常?!?/LI>
好了,,有了它們我們就可以完成這樣的代碼:
using System;
using System.Threading;
using System.Collections;
using System.Linq;
using System.Text;
class MonitorSample
{
//容器,一個(gè)只能容納一塊糖的糖盒子,。PS:現(xiàn)在MS已經(jīng)不推薦使用ArrayList,,
//支持泛型的List才是應(yīng)該在程序中使用的,我這里偷懶,,不想再去寫(xiě)一個(gè)Candy類了,。
private ArrayList _candyBox = new ArrayList(1);
private volatile bool _shouldStop = false; //用于控制線程正常結(jié)束的標(biāo)志
/// <summary>
/// 用于結(jié)束Produce()和Consume()在輔助線程中的執(zhí)行
/// </summary>
public void StopThread()
{
_shouldStop = true;
//這時(shí)候生產(chǎn)者/消費(fèi)者之一可能因?yàn)樵谧枞卸鴽](méi)有機(jī)會(huì)看到結(jié)束標(biāo)志,
//而另一個(gè)線程順利結(jié)束,,所以剩下的那個(gè)一定長(zhǎng)眠不醒,,需要我們?cè)谶@里嘗試叫醒它們。
//不過(guò)這并不能確保線程能順利結(jié)束,,因?yàn)榭赡芪覀儎倓偘l(fā)送信號(hào)以后,,線程才阻塞自己。
Monitor.Enter(_candyBox);
try
{
Monitor.PulseAll(_candyBox);
}
finally
{
Monitor.Exit(_candyBox);
}
}
/// <summary>
/// 生產(chǎn)者的方法
/// </summary>
public void Produce()
{
while(!_shouldStop)
{
Monitor.Enter(_candyBox);
try
{
if (_candyBox.Count==0)
{
_candyBox.Add("A candy");
Console.WriteLine("生產(chǎn)者:有糖吃啦,!");
//喚醒可能現(xiàn)在正在阻塞中的消費(fèi)者
Monitor.Pulse(_candyBox);
Console.WriteLine("生產(chǎn)者:趕快來(lái)吃?。?);
//調(diào)用Wait方法釋放對(duì)象上的鎖,,并使生產(chǎn)者線程狀態(tài)轉(zhuǎn)為WaitSleepJoin,,阻止該線程被CPU調(diào)用(跟Sleep一樣)
//直到消費(fèi)者線程調(diào)用Pulse(_candyBox)使該線程進(jìn)入到Running狀態(tài)
Monitor.Wait(_candyBox);
}
else //容器是滿的
{
Console.WriteLine("生產(chǎn)者:糖罐是滿的!");
//喚醒可能現(xiàn)在正在阻塞中的消費(fèi)者
Monitor.Pulse(_candyBox);
//調(diào)用Wait方法釋放對(duì)象上的鎖,,并使生產(chǎn)者線程狀態(tài)轉(zhuǎn)為WaitSleepJoin,,阻止該線程被CPU調(diào)用(跟Sleep一樣)
//直到消費(fèi)者線程調(diào)用Pulse(_candyBox)使生產(chǎn)者線程重新進(jìn)入到Running狀態(tài),此才語(yǔ)句返回
Monitor.Wait(_candyBox);
}
}
finally
{
Monitor.Exit(_candyBox);
}
Thread.Sleep(2000);
}
Console.WriteLine("生產(chǎn)者:下班啦,!");
}
/// <summary>
/// 消費(fèi)者的方法
/// </summary>
public void Consume()
{
//即便看到結(jié)束標(biāo)致也應(yīng)該把容器中的所有資源處理完畢再退出,,否則容器中的資源可能就此丟失
//不過(guò)這里_candyBox.Count是有可能讀到臟數(shù)據(jù)的,好在我們這個(gè)例子中只有兩個(gè)線程所以問(wèn)題并不突出
//正式環(huán)境中,,應(yīng)該用更好的辦法解決這個(gè)問(wèn)題,。
while (!_shouldStop || _candyBox.Count > 0)
{
Monitor.Enter(_candyBox);
try
{
if (_candyBox.Count==1)
{
_candyBox.RemoveAt(0);
if (!_shouldStop)
{
Console.WriteLine("消費(fèi)者:糖已吃完!");
}
else
{
Console.WriteLine("消費(fèi)者:還有糖沒(méi)吃,,馬上就完,!");
}
//喚醒可能現(xiàn)在正在阻塞中的生產(chǎn)者
Monitor.Pulse(_candyBox);
Console.WriteLine("消費(fèi)者:趕快生產(chǎn)!,!");
Monitor.Wait(_candyBox);
}
else
{
Console.WriteLine("消費(fèi)者:糖罐是空的,!");
//喚醒可能現(xiàn)在正在阻塞中的生產(chǎn)者
Monitor.Pulse(_candyBox);
Monitor.Wait(_candyBox);
}
}
finally
{
Monitor.Exit(_candyBox);
}
Thread.Sleep(2000);
}
Console.WriteLine("消費(fèi)者:都吃光啦,,下次再吃!");
}
static void Main(string[] args)
{
MonitorSample ss = new MonitorSample();
Thread thdProduce = new Thread(new ThreadStart(ss.Produce));
Thread thdConsume = new Thread(new ThreadStart(ss.Consume));
//Start threads.
Console.WriteLine("開(kāi)始啟動(dòng)線程,,輸入回車終止生產(chǎn)者和消費(fèi)者的工作……/r /n******************************************");
thdProduce.Start();
Thread.Sleep(2000); //盡量確保生產(chǎn)者先執(zhí)行
thdConsume.Start();
Console.ReadLine(); //通過(guò)IO阻塞主線程,,等待輔助線程演示直到收到一個(gè)回車
ss.StopThread(); //正常且優(yōu)雅的結(jié)束生產(chǎn)者和消費(fèi)者線程
Thread.Sleep(1000); //等待線程結(jié)束
while (thdProduce.ThreadState != ThreadState.Stopped)
{
ss.StopThread(); //線程還沒(méi)有結(jié)束有可能是因?yàn)樗旧硎亲枞模瑖L試使用StopThread()方法中的PulseAll()喚醒它,,讓他看到結(jié)束標(biāo)志
thdProduce.Join(1000); //等待生產(chǎn)這線程結(jié)束
}
while (thdConsume.ThreadState != ThreadState.Stopped)
{
ss.StopThread();
thdConsume.Join(1000); //等待消費(fèi)者線程結(jié)束
}
Console.WriteLine("******************************************/r/n輸入回車結(jié)束,!");
Console.ReadLine();
}
}
可能的幾種輸出(不是全部可能):
開(kāi)始啟動(dòng)線程,,輸入回車終止生產(chǎn)者和消費(fèi)者的工作……
******************************************
生產(chǎn)者:有糖吃啦,!
生產(chǎn)者:趕快來(lái)吃!,!
消費(fèi)者:還有糖沒(méi)吃,,馬上就完!
消費(fèi)者:趕快生產(chǎn)??!
生產(chǎn)者:下班啦!
消費(fèi)者:都吃光啦,,下次再吃,!
******************************************
輸入回車結(jié)束!
開(kāi)始啟動(dòng)線程,,輸入回車終止生產(chǎn)者和消費(fèi)者的工作……
******************************************
生產(chǎn)者:有糖吃啦,!
生產(chǎn)者:趕快來(lái)吃!,!
消費(fèi)者:糖已吃完,!
消費(fèi)者:趕快生產(chǎn)!,!
生產(chǎn)者:下班啦,!
消費(fèi)者:都吃光啦,下次再吃,!
******************************************
輸入回車結(jié)束,!
開(kāi)始啟動(dòng)線程,輸入回車終止生產(chǎn)者和消費(fèi)者的工作……
******************************************
生產(chǎn)者:有糖吃啦,!
生產(chǎn)者:趕快來(lái)吃?。?BR>消費(fèi)者:糖已吃完,!
消費(fèi)者:趕快生產(chǎn)?。?BR>生產(chǎn)者:有糖吃啦,!
生產(chǎn)者:趕快來(lái)吃??!
消費(fèi)者:還有糖沒(méi)吃,馬上就完,!
消費(fèi)者:趕快生產(chǎn)?。?BR>生產(chǎn)者:下班啦,!
消費(fèi)者:都吃光啦,,下次再吃!
******************************************
輸入回車結(jié)束,!
有興趣的話你還可以嘗試修改生產(chǎn)者和消費(fèi)者的啟動(dòng)順序,,嘗試下其它的結(jié)果(比如糖罐為空)。其實(shí)生產(chǎn)者和消費(fèi)者方法中那個(gè) Sleep(2000)也是為了方便手工嘗試出不同分支的執(zhí)行情況,,輸出中的空行就是我敲入回車讓線程中止的時(shí)機(jī),。
你可能已經(jīng)發(fā)現(xiàn),除非消費(fèi)者先于生產(chǎn)者啟動(dòng),,否則我們永遠(yuǎn)不會(huì)看到消費(fèi)者說(shuō)“糖罐是空的,!”,這是因?yàn)橄M(fèi)者在吃糖以后把自己阻塞了,,直到生產(chǎn)者生產(chǎn)出糖塊后喚醒自己,。另一方面,生產(chǎn)者即便先于消費(fèi)者啟動(dòng),,在這個(gè)例子中我們也永遠(yuǎn)不會(huì)看到生產(chǎn)者說(shuō)“糖罐是滿的,!”,因?yàn)槌跏继枪逓榭涨疑a(chǎn)者在生產(chǎn)后就把自己阻塞了,。
題外話1:
是不是覺(jué)得生產(chǎn)者判斷糖罐是滿的,、消費(fèi)者檢查出糖罐是空的分支有些多余?
想想,,如果糖罐初始也許并不為空,,又或者消費(fèi)者先于生產(chǎn)者執(zhí)行,那么它們就會(huì)派上用場(chǎng),。這畢竟只是一個(gè)例子,,我們?cè)跊](méi)有任何限制條件下設(shè)計(jì)了這個(gè)環(huán)環(huán)相扣的簡(jiǎn)單場(chǎng)景,所以讓這兩個(gè)分支“顯得”有些多余,,但大多數(shù)真實(shí)情況并不如此,。
在實(shí)際應(yīng)用中,生產(chǎn)者往往代表負(fù)責(zé)從某處簡(jiǎn)單接收資源的線程,,比如來(lái)自網(wǎng)絡(luò)的指令,、從服務(wù)器返回的查詢等等;而消費(fèi)者線程需要負(fù)責(zé)解析指令,、解析返回的查詢結(jié)果,,然后存儲(chǔ)到本地?cái)?shù)據(jù)庫(kù),、文件或者呈現(xiàn)給用戶等等。消費(fèi)者線程的任務(wù)往往更復(fù)雜,,執(zhí)行時(shí)間更長(zhǎng),,為了提高程序的整體執(zhí)行效率,消費(fèi)者線程往往會(huì)多于生產(chǎn)者線程,,可能3對(duì)1,,也可能5對(duì)2……
CPU的隨機(jī)調(diào)度,可能會(huì)造成各種各樣的情況,。你基本上是無(wú)法預(yù)測(cè)一段代碼在被調(diào)用時(shí),,與之相關(guān)的外部環(huán)境是怎樣的,所以完備的處理每一個(gè)分支是必要的,。
另一方面,,即便一個(gè)分支的情況不是我們?cè)O(shè)計(jì)中期望發(fā)生的,但是由于某種現(xiàn)在無(wú)法預(yù)見(jiàn)的錯(cuò)誤,,造成本“不可能”、“不應(yīng)該”出現(xiàn)的分支得以執(zhí)行,,那么在這個(gè)分支的代碼可以保障你的業(yè)務(wù)邏輯可以在錯(cuò)誤的異常情況下得以修正,,至少你也可以報(bào)警避免更大的錯(cuò)誤。
所以總是建議給每個(gè)if都寫(xiě)上else分支,,這除了讓你的代碼顯得更加僅僅有條,、邏輯清晰外,還可能給你帶來(lái)額外的擴(kuò)展性和健壯性,。就像在前一篇中所提到的,,不要因?yàn)閯e人(你所寫(xiě)類的使用者)的“錯(cuò)誤”(誰(shuí)讓你給別人這個(gè)機(jī)會(huì)呢?)連累自己,!
題外話2:
你可以用微軟的建議用 lock(_candyBox){...} 替代上面代碼中的 Monitor.Enter(_candyBox);try{...}finally{Monitor.Exit(_candyBox);},,這里我不做任何反對(duì)。不過(guò)在更多時(shí)候,,你核能會(huì)需要在finally里做更多的事情,,而不只是Exit那么簡(jiǎn)單,所以即便用了lock,,你還得自己寫(xiě) try/finally,。
如果你的頭已經(jīng)有些暈了,那么馬上跳過(guò)這個(gè)題外話,,下面說(shuō)的跟線程同步毫無(wú)關(guān)系,。這個(gè)題外話其實(shí)想引申到 using。這個(gè)C#特有的(其它.net語(yǔ)言沒(méi)有類似語(yǔ)法)關(guān)鍵字,,它會(huì)幫你自動(dòng)調(diào)用所有實(shí)現(xiàn)了IDisposable接口類上的Dispose()方法,。跟lock類似,,using(obj) {//do something}等效于一個(gè)如下的try/finally語(yǔ)句塊:
SS obj = new SS();
try
{
//use obj to do something
}
finally
{
obj.Dispose();
}
微軟一廂情愿的希望通過(guò)using避免程序員忘記調(diào)用Dispose()去釋放該類所占用的那些資源,包括托管的和非托管的(磁盤IO,、網(wǎng)絡(luò)IO,、數(shù)據(jù)庫(kù)連接IO等等),你通常會(huì)在關(guān)于磁盤操作的類,、各種Stream,、網(wǎng)絡(luò)操作相關(guān)的類、數(shù)據(jù)庫(kù)驅(qū)動(dòng)類上找到這個(gè)方法,。Dispose()里主要是替你 Disconnet()/Close()掉這些資源,,但是這些Dispose()方法常常是由微軟之外的公司編寫(xiě)的,比如Oracle的.Net驅(qū)動(dòng),。你能確信Oracle的程序員非常了解Dispose()在.net中的重要含義么,?回頭來(lái)說(shuō),就算是微軟自己的程序員,,難道就不會(huì)犯錯(cuò)誤嗎,?跟lock中提到的SynRoot實(shí)現(xiàn)一樣,你根本不知道你所使用類的Dispose()是否是正確的,,也無(wú)法確保下一個(gè)版本的Dispose()不會(huì)悄悄的改變…… 對(duì)于這些敏感的資源,,自己老老實(shí)實(shí)去Disconnect()/Close(),再老老實(shí)實(shí)的去Dispose(),。事實(shí)上finally需要做的事情也往往不只是一個(gè)Dispose(),。
一句話,關(guān)于using,,堅(jiān)決反對(duì),。
就到這里吧,好累~