預(yù)備知識(shí):線程的相關(guān)概念和知識(shí),有多線程編碼的初步經(jīng)驗(yàn)。 一個(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è)東西?
Lock關(guān)鍵字 C#提供lock關(guān)鍵字實(shí)現(xiàn)臨界區(qū),MSDN里給出的用法: Object thisLock = new Object(); 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; class Account class Test 這個(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()方法專門(mén)執(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)然也可以不用)。 題外話: Lock使用的建議 關(guān)于使用Lock微軟給出的一些建議。你能夠在MSDN上找到這么一段話: 通常,應(yīng)避免鎖定 public 類型,否則實(shí)例將超出代碼的控制范圍。常見(jiàn)的結(jié)構(gòu) lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 違反此準(zhǔn)則: lock(this)的問(wèn)題我是這么理解:
至于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 題外話:
另外,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)樗拇蟛糠址椒ǘ加胠ock來(lái)進(jìn)行了同步處理(你會(huì)不會(huì)想那么SyncRoot顯得多余?別急。)。比如,Add方法會(huì)類似于: public override void Add(objectkey,objectvalue) 不過(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(); 提供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 自已寫(xiě)一個(gè)類,用自己的syncRoot封裝一個(gè)線程安全的容器。 |
|
來(lái)自: 昵稱26960982 > 《C#》