上章主要講排他鎖的直接使用方式。但實際當中全部都用鎖又太浪費了,或者排他鎖粒度太大了。 這一次我們說說升級鎖和原子操作。
目錄
1:volatile
2: Interlocked
3:ReaderWriterLockSlim
4:總結
一:volatile
簡單來說: volatile關鍵字是告訴c#編譯器和JIT編譯器,不對volatile標記的字段做任何的緩存。確保字段讀寫都是原子操作,最新值。
這不就是鎖嗎? 其這貨它根本不是鎖, 它的原子操作是基于CPU本身的,非阻塞的。 因為32位CPU執行賦值指令,數據傳輸最大寬度4個字節。
所以只要在4個字節以下讀寫操作的,32位CPU都是原子操作。volatile 它就是利用這個特性來的。
好殘酷的事實?不然,微軟大法這樣是為了提高JIT性能效率,對有些數據進行緩存了(多線程下)。
看上面的例子,我們定義8個字節長度score2就不行了。 因為8個字節,32位CPU就分成2個指令執行了。自然就無法保證原子操作了。
這么細節的,忘了怎么辦,那豈不是坑人啊。 于是微軟大法直接一棍子打死,限制4個字節以下的類型字段才能用volatile,具體什么、看msdn吧。
那今天我知道了。我編譯平臺改成64位上,只在64位CPU用volatile int64,行不行? 不行,編譯器報錯。說了一棍子打死了。。
(^._.^)ノ 好吧,其實可以用IntPtr這個。
volatile多數情況下很有用處的,畢竟鎖的性能開銷還是很大的。我們可以把當成輕量級的鎖,根據具體場景合理使用,能提高不少程序性能。
線程中的Thread.VolatileRead 和Thread.VolatileWrite 就是volatile的復雜版。
二:Interlocked
MSDN 描述:為多個線程共享的變量提供原子操作。主要函數如下:
Interlocked.Increment 原子操作,遞增指定變量的值并存儲結果。
Interlocked.Decrement 原子操作,遞減指定變量的值并存儲結果。
Interlocked.Add 原子操作,添加兩個整數并用兩者的和替換第一個整數
Interlocked.CompareExchange(ref a, b, c); 原子操作,a參數和c參數比較, 相等b替換a,不相等不替換。
基本用法就不多說了。直接來段CLR via C# interlock anything的例子:
//高并發下,線程被搶占情況下,target值會發生改變。
//target startVal相等說明沒改變。desiredVal 直接替換。
currentVal = Interlocked.CompareExchange(ref target, desiredVal, startVal);
} while (startVal != currentVal); //不相等說明,target值已經被其他線程改動。自旋繼續。
return desiredVal;
}
三:ReaderWriterLockSlim
假如我們有份緩存數據A,如果每次都不管任何操作lock一下,那么我的這份緩存A就永遠只能單線程讀寫了, 這在Web高并發下是不能忍受的。
那有沒有一種辦法我只在寫入時進入獨占鎖呢,讀操作時不限制線程數量呢?答案就是我們的ReaderWriterLockSlim主角,讀寫鎖。
ReaderWriterLockSlim 其中一種鎖EnterUpgradeableReadLock最關鍵 即可升級鎖。
它呢允許你先進入讀鎖,發現緩存A不一樣了, 再進入寫鎖,寫入后退回讀鎖模式。
ps: 這里注意下net 3.5之前有個ReaderWriterLock 性能較差。推薦使用升級版的 ReaderWriterLockSlim 。
LockRecursionPolicy.NoRecursion 不支持,發現遞歸會拋異常。
LockRecursionPolicy.SupportsRecursion 即支持遞歸模式,線程鎖中繼續在使用鎖。
這種模式極易容易死鎖,比如讀鎖里面使用寫鎖。
下面是直接拿msdn的緩存例子了,加了簡單注釋。
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
};
}
四:總結
多線程實際開發當中,往往測試沒問題,一到生產環境,并發高了就容易出問題, 一定注意。
本文參考CLR via C#。
新聞熱點
疑難解答