C#线程同步(1)- 临界区&Lock .
預備知識:線程的相關概念和知識,有多線程編碼的初步經驗。
一個機會,索性把線程同步的問題在C#里面的東西都粗略看了下。
第一印象,C#關于線程同步的東西好多,保持了C#一貫的大雜燴和四不象風格(Java/Delphi)。臨界區跟Java差不多只不過關鍵字用lock替代了synchronized,然后又用Moniter的Wait/Pulse取代了Object的Wait/Notify,另外又搞出來幾個Event……讓人甚是不明了。不管那么多,一個一個來吧。
臨界區(Critical Section)
是一段在同一時候只被一個線程進入/執行的代碼。為啥要有這個東西?
Lock關鍵字
C#提供lock關鍵字實現臨界區,MSDN里給出的用法:
Object thisLock = new Object();
lock (thisLock)
{
?? // Critical code section
}
lock實現臨界區是通過“對象鎖”的方式,注意是“對象”,所以你只能鎖定一個引用類型而不能鎖定一個值類型。第一個執行該代碼的線程,成功獲取對這個對象的鎖定,進而進入臨界區執行代碼。而其它線程在進入臨界區前也會請求該鎖,如果此時第一個線程沒有退出臨界區,對該對象的鎖定并沒有解除,那么當前線程會被阻塞,等待對象被釋放。
既然如此,在使用lock時,要注意不同線程是否使用同一個“鎖”作為lock的對象。現在回頭來看MSDN的這段代碼似乎很容易讓人誤解,容易讓人聯想到這段代碼是在某個方法中存在,以為thisLock是一個局部變量,而局部變量的生命周期是在這個方法內部,所以當不同線程調用這個方法的時候,他們分別請求了不同的局部變量作為鎖,那么他們都可以分別進入臨界區執行代碼。因此在MSDN隨后真正的示例中,thisLock實際上是一個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();
??????? }
??? }
}
這個例子中,Account對象只有一個,所以臨界區所請求的“鎖”是唯一的,因此用類的成員變量是可以實現互斥意圖的,其實用大家通常喜歡的lock(this)也未嘗不可,也即請求這個Account實例本身作為鎖。但是如果在某種情況你的類實例并不唯一或者一個類的幾個方法之間都必須要互斥,那么就要小心了。必須牢記一點,所有因為同一互斥資源而需要互斥的操作,必須請求“同一把鎖”才有效。
假設這個Account類并不只有一個Withdraw方法修改balance,而是用Withdraw()來特定執行取款操作,另有一個Deposit()方法專門執行存款操作。很顯然這兩個方法必須是互斥執行的,所以這兩個方法中所用到的鎖也必須一致;不能一個用thisLock,另一個重新用一個private Object thisLock1 = new Object()。再進一步,其實這個操作場景下各個互斥區存在的目的是因為有“Balance”這個互斥資源,所有有關Balance的地方應該都是互斥的(如果你不介意讀取操作讀到的是臟數據的話,當然也可以不用)。
題外話:
這么看來其實用Balance本身作為鎖也許更為符合“邏輯”,lock住需要互斥的資源本身不是更好理解么?不過這里Balance是一個值類型,你并不能直接對它lock(你可能需要用到volatile關鍵字,它能在單CPU的情況下確保只有一個線程修改一個變量)。
Lock使用的建議
關于使用Lock微軟給出的一些建議。你能夠在MSDN上找到這么一段話:
通常,應避免鎖定 public 類型,否則實例將超出代碼的控制范圍。常見的結構 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 違反此準則:
1.如果實例可以被公共訪問,將出現 lock (this) 問題。
2.如果 MyType 可以被公共訪問,將出現 lock (typeof (MyType)) 問題。
3.由于進程中使用同一字符串的任何其他代碼將共享同一個鎖,所以出現 lock("myLock") 問題。
4.最佳做法是定義 private 對象來鎖定, 或 private static 對象變量來保護所有實例所共有的數據。
lock(this)的問題我是這么理解:
MyType的問題跟lock(this)差不多理解,不過比lock(this)更嚴重。因為Lock(typeof(MyType))鎖定住的對象范圍更為廣泛,由于一個類的所有實例都只有一個類對象(就是擁有Static成員的那個對象實例),鎖定它就鎖定了該對象的所有實例。同時lock(typeof(MyType))是個很緩慢的過程,并且類中的其他線程、甚至在同一個應用程序域中運行的其他程序都可以訪問該類型對象,因此,它們都有可能鎖定類對象,完全阻止你代碼的執行,導致你自己代碼的掛起或者死鎖。
至于lock("myLock"),是因為在.NET中字符串會被暫時存放。如果兩個變量的字符串內容相同的話,.NET會把暫存的字符串對象分配給該變量。所以如果有兩個地方都在使用lock(“my lock”)的話,它們實際鎖住的是同一個對象。
.NET集合類對lock的支持
在多線程環境中,常會碰到的互斥資源應該就是一些容器/集合。因此.NET在一些集合類中(比如ArrayList,HashTable,Queue,Stack,包括新增的支持泛型的List)已經提供了一個供lock使用的對象SyncRoot。
在.Net1.1中大多數集合類的SyncRoot屬性只有一行代碼:return this,這樣和lock(集合的當前實例)是一樣的。不過ArrayList中的SyncRoot有所不同(這個并不是我反編譯的,我并沒有驗證這個說法):
get
{?
? if(this._syncRoot==null)
? {
??? Interlocked.CompareExchange(refthis._syncRoot,newobject(),null);
? }
? returnthis._syncRoot;
}
題外話:
上面反編譯的ArrayList的代碼,引出了個Interlocked類,即互鎖操作,用以對某個內存位置執行的簡單原子操作。舉例來說在大多數計算機上,增加變量操作不是一個原子操作,需要執行下列步驟:
線程可能會在執行完前兩個步驟后被奪走CPU時間,然后由另一個線程執行所有三個步驟。當第一個線程重新再開始執行時,它改寫實例變量中的值,造成第二個線程執行增減操作的結果丟失。這根我們上面提到的銀行賬戶余額的例子是一個道理,不過是更微觀上的體現。我們使用該類提供了的Increment和Decrement方法就可以避免這個問題。
另外,Interlocked類上提供了其它一些能保證對相關變量的操作是原子性的方法。如Exchange()可以保證指定變量的值交換操作的原子性,Read()保證在32位操作系統中對64位變量的原子讀取。而這里使用的CompareExchange方法組合了兩個操作:保證了比較和交換操作按原子操作執行。此例中CompareExchange方法將當前syncRoot和null做比較,如果相等,就用new object()替換SyncRoot。
在現代處理器中,Interlocked 類的方法經常可以由單個指令來實現,因此它們的執行性能非常高。雖然Interlocked沒有直接提供鎖定或者發送信號的能力,但是你可以用它編寫鎖和信號,從而編寫出高效的非阻止并發的應用程序。但是這需要復雜的低級別編程能力,因此大多數情況下使用lock或其它簡單鎖是更好的選擇。
?
?
?
看到這里是不是已經想給微軟一耳光了?一邊教導大家不要用lock(this),一邊竟然在基礎類庫中大量使用……呵呵,我只能說據傳從.Net2.0開始SyncRoot已經是會返回一個單獨的類了,想來大約應該跟ArrayList那種實現差不多,有興趣的可以反編譯驗證下。
這里想說,代碼是自己的寫的,最好減少自己代碼對外部環境的依賴,事實證明即便是.Net基礎庫也不是那么可靠。自己能想到的問題,最好自己寫代碼去處理,需要鎖就自己聲明一個鎖;不再需要一個資源那么自己代碼去Dispose掉(如果是實現IDisposable接口的)……不要想著什么東西系統已經幫你做了。你永遠無法保證你的類將會在什么環境下被使用,你也無法預見到下一版的Framework是否偷偷改變了實現。當你代碼莫名其妙不Work的時候,你是很難找出由這些問題引發的麻煩。只有你代碼足夠的獨立(這里沒有探討代碼耦合度的問題),才能保證它足夠的健壯;別人代碼的修改(哪怕是你看來“不當”的修改),造成你的Code無法工作不是總有些可笑么(我還想說“蒼蠅不叮無縫的蛋”“不要因為別人的錯誤連累自己”)?
一些集合類中還有一個方法是和同步相關的:Synchronized,該方法返回一個集合的內部類,該類是線程安全的,因為他的大部分方法都用lock來進行了同步處理(你會不會想那么SyncRoot顯得多余?別急。)。比如,Add方法會類似于:
public override void Add(objectkey,objectvalue)?
{?
? lock(this._table.SyncRoot)
? {
??? this._table.Add(key,value);
? }?
}
不過即便是這個Synchronized集合,在對它進行遍歷時,仍然不是一個線程安全的過程。當你遍歷它時,其他線程仍可以修改該它(Add、Remove),可能會導致諸如下標越界之類的異常;就算不出錯,你也可能讀到臟數據。若要在遍歷過程中保證線程安全,還必須在整個遍歷過程中鎖定集合,我想這才是SynRoot存在的目的吧:
Queue myCollection = newQueue();
lock(myCollection.SyncRoot)
{
? foreach(ObjectiteminmyCollection)
? {?
??? //Insert your code here.
? }?
}
提供SynRoot是為了把這個已經“線程安全”的集合內部所使用的“鎖”暴露給你,讓你和它內部的操作使用同一把鎖,這樣才能保證在遍歷過程互斥掉其它操作,保證你在遍歷的同時沒有可以修改。另一個可以替代的方法,是使用集合上提供的靜態ReadOnly()方法,來返回一個只讀的集合,并對它進行遍歷,這個返回的只讀集合是線程安全的。
到這里似乎關于集合同步的方法似乎已經比較清楚了,不過如果你是一個很迷信MS基礎類庫的人,那么這次恐怕又會失望了。微軟決定所有從那些自Framwork 3.0以來加入的支持泛型的集合中,如List,取消掉創建同步包裝器的能力,也就是它們不再有Synchronized,IsSynchronized也總會返回false;而ReadOnly這個靜態方法也變為名為AsReadOnly的實例方法。作為替代,MS建議你仍然使用lock關鍵字來鎖定整個集合。
至于List之類的泛型集合SyncRoot是怎樣實現的,MSDN是這樣描述的“在 List<(Of <(T>)>) 的默認實現中,此屬性始終返回當前實例。”,趕緊去吐血吧!
自己的SyncRoot
還是上面提過的老話,靠自己,以不變應萬變:
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);
??? }
? }
? //...
}
自已寫一個類,用自己的syncRoot封裝一個線程安全的容器。
轉載于:https://www.cnblogs.com/huoguofeng/archive/2013/04/03/2997495.html
總結
以上是生活随笔為你收集整理的C#线程同步(1)- 临界区&Lock .的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2018工银教师信用卡权益 尊享礼遇媲美
- 下一篇: 矩阵快速幂 zoj-3690 Choos