多執行緒中的鎖系統(二)-volatile、Interlocked、ReaderWriterLockSlim

蘑菇先生發表於2015-01-01

     上章主要講排他鎖的直接使用方式。但實際當中全部都用鎖又太浪費了,或者排他鎖粒度太大了,本篇主要介紹下升級鎖和原子操作。

閱讀目錄

  1. volatile
  2. Interlocked
  3. ReaderWriterLockSlim

volatile

簡單來說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以前的版本。

Interlocked

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;
        }

ReaderWriterLockSlim

假如有份快取資料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
        };
    }

 

多執行緒實際開發當中一直是個難點,特別是併發量比較高的情況下,這需要使用時尤為注意。

相關文章