小分享:我有幾張阿裡雲優惠券,用券購買或者升級阿裡雲相應產品最多可以優惠五折!領券地址:https://promotion.aliyun.com/ntms/act/ambassador/sharetouser.html?userCode=ohmepe03
上章主要講排他鎖的直接使用方式。但實際當中全部都用鎖又太浪費了,或者排他鎖粒度太大了,本篇主要介紹下升級鎖和原子操作。
簡單來說volatile關鍵字是告訴c#編譯器和JIT編譯器,不對volatile標記的字段做任何的緩存。確保字段讀寫都是原子操作,最新值。
從功能上看起到鎖的作用,但它不是鎖, 它的原子操作是基於CPU本身的,非阻塞的。 因為32位CPU執行賦值指令,數據傳輸最大寬度4個字節。
所以只要在4個字節以下讀寫操作的,32位CPU都是原子操作,volatile 是利用這個特性來保證其原子操作的。
這樣的目的是為了提高JIT性能效率,對有些數據進行緩存了(多線程下)。
//正確 public volatile Int32 score1 = 1; //報錯 public volatile Int64 score2 = 1;
如上,我們嘗試定義了8個字節長度score2,會拋出異常。 因為8個字節32位CPU就分成2個指令執行了,所以就無法保證其原子操作了。
如果把編譯平台改成64位,同樣不可以使用,C#限制4個字節以下的類型字段才能用volatile。
還一種方法是使用特定平台的整數類型IntPtr,這樣volatile即可作用到64位上了。
volatile多數情況下很有用處的,畢竟鎖的性能開銷還是很大的。可以把當成輕量級的鎖,根據具體場景合理使用,能提高不少程序性能。
線程中的Thread.VolatileRead 和Thread.VolatileWrite 是volatile以前的版本。
MSDN 描述:為多個線程共享的變量提供原子操作。主要函數如下:
Interlocked.Increment 原子操作,遞增指定變量的值並存儲結果。
Interlocked.Decrement 原子操作,遞減指定變量的值並存儲結果。
Interlocked.Add 原子操作,添加兩個整數並用兩者的和替換第一個整數
Interlocked.CompareExchange(ref a, b, c); 原子操作,a參數和c參數比較, 相等b替換a,不相等不替換。
下面是個interlock anything的例子:
public static int Maximum(ref int target, int value) { int currentVal = target, startVal, desiredVal; //記錄前後值 do { startVal = currentVal; //記錄循環迭代的初始值。 desiredVal = Math.Max(startVal, value); //基於startVal和value計算期望值desiredVal //高並發下,線程被搶占情況下,target值會發生改變。 //target startVal相等說明沒改變。desiredVal 直接替換。 currentVal = Interlocked.CompareExchange(ref target, desiredVal, startVal); } while (startVal != currentVal); //不相等說明,target值已經被其他線程改動。自旋繼續。 return desiredVal; }
假如有份緩存數據A,如果每次都不管任何操作lock一下,那麼我的這份緩存A就永遠只能單線程讀寫了, 這在Web高並發下是不能忍受的。
那有沒有一種辦法我只在寫入時進入獨占鎖呢,讀操作時不限制線程數量呢?答案就是我們的ReaderWriterLockSlim主角,讀寫鎖。
ReaderWriterLockSlim 其中一種鎖EnterUpgradeableReadLock最關鍵 即可升級鎖。
它允許你先進入讀鎖,發現緩存A不一樣了, 再進入寫鎖,寫入後退回讀鎖模式。
ps: 這裡注意下net 3.5之前有個ReaderWriterLock 性能較差。推薦使用升級版的 ReaderWriterLockSlim 。
//實例一個讀寫鎖 ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
上面實例一個讀寫鎖,這裡注意的是構造函數的枚舉。
LockRecursionPolicy.NoRecursion 不支持,發現遞歸會拋異常。
LockRecursionPolicy.SupportsRecursion 即支持遞歸模式,線程鎖中繼續在使用鎖。
cacheLock.EnterReadLock(); //do cacheLock.EnterReadLock(); //do cacheLock.ExitReadLock(); cacheLock.ExitReadLock();
這種模式極易容易死鎖,比如讀鎖裡面使用寫鎖。
cacheLock.EnterReadLock(); //do cacheLock.EnterWriteLock(); //do cacheLock.ExitWriteLock(); cacheLock.ExitReadLock();
下面是msdn的緩存例子了,加了注釋。
public class SynchronizedCache { private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim(); private Dictionary<int, string> innerCache = new Dictionary<int, string>(); public string Read(int key) { //進入讀鎖,允許其他所有的讀線程,寫入線程被阻塞。 cacheLock.EnterReadLock(); try { return innerCache[key]; } finally { cacheLock.ExitReadLock(); } } public void Add(int key, string value) { //進入寫鎖,其他所有訪問操作的線程都被阻塞。即寫獨占鎖。 cacheLock.EnterWriteLock(); try { innerCache.Add(key, value); } finally { cacheLock.ExitWriteLock(); } } public bool AddWithTimeout(int key, string value, int timeout) { //超時設置,如果在超時時間內,其他寫鎖還不釋放,就放棄操作。 if (cacheLock.TryEnterWriteLock(timeout)) { try { innerCache.Add(key, value); } finally { cacheLock.ExitWriteLock(); } return true; } else { return false; } } public AddOrUpdateStatus AddOrUpdate(int key, string value) { //進入升級鎖。 同時只能有一個可升級鎖線程。寫鎖,升級鎖都被阻塞,但允許其他讀取數據的線程。 cacheLock.EnterUpgradeableReadLock(); try { string result = null; if (innerCache.TryGetValue(key, out result)) { if (result == value) { return AddOrUpdateStatus.Unchanged; } else { //升級成寫鎖,其他所有線程都被阻塞。 cacheLock.EnterWriteLock(); try { innerCache[key] = value; } finally { //退出寫鎖,允許其他讀線程。 cacheLock.ExitWriteLock(); } return AddOrUpdateStatus.Updated; } } else { cacheLock.EnterWriteLock(); try { innerCache.Add(key, value); } finally { cacheLock.ExitWriteLock(); } return AddOrUpdateStatus.Added; } } finally { //退出升級鎖。 cacheLock.ExitUpgradeableReadLock(); } } public enum AddOrUpdateStatus { Added, Updated, Unchanged }; }
多線程實際開發當中一直是個難點,特別是並發量比較高的情況下,這需要使用時尤為注意。
參考頁面:http://qingqingquege.cnblogs.com/p/5933752.html