並發資料結構 : .NET Framework 中提供的讀寫鎖

來源:互聯網
上載者:User
文章目錄
  • 在多線程編程時,開發人員經常會遭遇多個線程讀寫某個資源的情況。這就需要進行線程同步來保證安全執行緒。一般情況下,我們的同步措施是使用鎖機制。但是,假如線程只對資源進行讀取操作,那麼根本不需要使用鎖;反之,假如線程只對資源進行寫入操作,則應當使用互斥鎖(比如使用 Monitor 類等)。還有一種情況,就是存在多個線程對資源進行讀取操作,同時每次只有一個線程對資源進行獨佔寫入操作。這正是本文主題--讀寫鎖的用武之地。
在多線程編程時,開發人員經常會遭遇多個線程讀寫某個資源的情況。這就需要進行線程同步來保證安全執行緒。一般情況下,我們的同步措施是使用鎖機制。但是,假如線程只對資源進行讀取操作,那麼根本不需要使用鎖;反之,假如線程只對資源進行寫入操作,則應當使用互斥鎖(比如使用 Monitor 類等)。還有一種情況,就是存在多個線程對資源進行讀取操作,同時每次只有一個線程對資源進行獨佔寫入操作。這正是本文主題--讀寫鎖的用武之地。ReaderWriterLock 類

.NET Framework BCL 在 1.1 版本時,給我們提供了一個 ReaderWriterLock 類來面對此種情景。但是很遺憾,Microsoft 官方不推薦使用該類。Jeffrey Richter 也在他的《CLR via C#》一書中對它進行了嚴厲的批判。下面是該類不受歡迎的主要原因:

  • 效能。這個類實在是太慢了。比如它的 AcquireReaderLock 方法比 Monitor 類的 Enter 方法要慢 5 倍左右,而等待爭奪寫鎖甚至比 Monitor 類慢 6 倍。
  • 策略。假如某個線程完成寫入操作後,同時面臨讀線程和寫線程等待處理。ReaderWriterLock 會優先釋放讀線程,而讓寫線程繼續等待。但我們使用讀寫鎖是因為存在大量的讀線程和非常少的寫線程,這樣寫線程很可能必須長時間地等待,造成寫線程饑餓,不能及時更新資料。更槽糕的情況是,假如寫線程一直等待,就會造成活鎖。反之,我們讓 ReaderWriterLock 採取寫線程優先的策略。如果存在多個寫線程,而讀線程數量稀少,也會造成讀線程饑餓。幸運的是,現實實踐中,這種情況很少出現。一旦發生這種情況,我們可以採取互斥鎖的辦法。
  • 遞迴。ReaderWriterLock 類支援鎖遞迴。這就意味著該鎖清楚的知道目前哪個線程擁有它。假如擁有該鎖的線程遞迴嘗試獲得該讀寫鎖,遞迴演算法允許該線程獲得該讀寫鎖,並且增加獲得該鎖的計數。然而該線程必須釋放該鎖相同的次數以便線程不再擁有該鎖。儘管這看起來是個很好的特性,但是實現這個“特性”代價太高。首先,因為多個讀線程可以同時擁有該讀寫鎖,這必須讓該鎖為每個線程保持計數。此外,還需要額外的記憶體空間和時間來更新計數。這個特性對 ReaderWriterLock 類可憐的效能貢獻極大。其次,有些良好的設計需要一個線程在此處獲得該鎖,然後在別處釋放該鎖(比如 .NET 的非同步編程架構)。因為這個遞迴特性,ReaderWriterLock 不支援這種編程架構。
  • 資源泄漏。在 .NET 2.0 之前的版本中, ReaderWriterLock 類會造成核心對象泄露。這些對象只有在進程終止後才能再次回收。幸運的是,.NET 2.0 修正了這個 Bug 。

此外,ReaderWriterLock 還有個令人擔心的危險的非原子性操作。它就是 UpgradeToWriteLock 方法。這個方法實際上在更新到寫鎖前先釋放了讀鎖。這就讓其他線程有機會在此期間乘虛而入,從而獲得讀寫鎖且改變狀態。如果先更新到寫鎖,然後釋放讀鎖。假如兩個線程同時更新將會導致另外一個線程死結。

所以 Microsoft 決定構建一個新類來一次性解決上述所有問題,這就是 ReaderWriterLockSlim 類。本來可以在原有的 ReaderWriterLock 類上修正錯誤,但是考慮到相容性和已存在的 API ,Microsoft 放棄了這種做法。當然也可以標記 ReaderWriterLock 類為 Obsolete,但是由於某些原因,這個類還有存在的必要。

ReaderWriterLockSlim 類

新的 ReaderWriterLockSlim 類支援三種鎖定模式:Read,Write,UpgradeableRead。這三種模式對應的方法分別是 EnterReadLock,EnterWriteLock,EnterUpgradeableReadLock 。再就是與此對應的 TryEnterReadLock,TryEnterWriteLock,TryEnterUpgradeableReadLock,ExitReadLock,ExitWriteLock,ExitUpgradeableReadLock。Read 和 Writer 鎖定模式比較簡單易懂:Read 模式是典型的共用鎖定定模式,任意數量的線程都可以在該模式下同時獲得鎖;Writer 模式則是互斥模式,在該模式下只允許一個線程進入該鎖。UpgradeableRead 鎖定模式可能對於大多數人來說比較新鮮,但是在資料庫領域卻眾所周知。

這個新的讀寫鎖類效能跟 Monitor 類大致相當,大概在 Monitor 類的 2 倍之內。而且新鎖優先讓寫線程獲得鎖,因為寫操作的頻率遠小於讀操作。通常這會導致更好的延展性。起初,ReaderWriterLockSlim 類在設計時考慮到相當多的情況。比如在早期 CTP 的代碼還提供了PrefersReaders, PrefersWritersAndUpgrades 和 Fifo 等競爭策略。但是這些策略雖然添加起來非常簡單,但是會導致情況非常的複雜。所以 Microsoft 最後決定提供一個能夠在大多數情況下良好工作的簡單模型。

ReaderWriterLockSlim 的更新鎖定

現在讓我們更加深入的討論一下更新模型。UpgradeableRead 鎖定模式允許安全的從 Read 或 Write 模式下更新。還記得先前 ReaderWriterLock 的更新是非原子性,危險的操作嗎(尤其是大多數人根本沒有意識到這點)?現在提供的新讀寫鎖既不會破壞原子性,也不會導致死結。新鎖一次只允許一個線程處在 UpgradeableRead 模式下。

一旦該讀寫鎖處在 UpgradeableRead 模式下,線程就能讀取某些狀態值來決定是否降級到 Read 模式或升級到 Write 模式。注意應當儘可能快的作出這個決定:持有 UpgradeableRead 鎖會強制任何新的讀請求等待,儘管已存在的讀取操作仍然活躍。遺憾的是,CLR 團隊移除了 DowngradeToRead 和 UpgradeToWrite 兩個方法。如果要降級到讀鎖,只要簡單的在 ExitUpgradeableReadLock 方法後緊跟著調用 EnterReadLock 方法即可:這可以讓其他的 Read 和 UpgradeableRead 獲得完成先前應當持有卻被 UpgradeableRead 鎖持有的操作。如果要升級到寫鎖,只要簡單調用 EnterWriteLock 方法即可:這可能要等待,直到不再有任何線程在 Read 模式下持有鎖。不像降級到讀鎖,必須調用 ExitUpgradeableReadLock。在 Write 模式下不必非得調用 ExitUpgradeableReadLock。但是為了形式統一,最好還是調用它。比如下面的代碼:

using System;using System.Linq;using System.Threading;namespace Lucifer.CSharp.Sample{    class Program    {        private ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();        void Sample()        {            bool isUpdated = true;            rwLock.EnterUpgradeableReadLock();            try             {                if (/* … 讀取狀態值來決定是否更新 … */)                 {                    rwLock.EnterWriteLock();                    try                     {                        //… 寫入狀態值 …                    }                     finally                     {                        rwLock.ExitWriteLock();                    }                }                 else                 {                    rwLock.EnterReadLock();                    rwLock.ExitUpgradeableReadLock();                    isUpdated = false;                    try                     {                        //… 讀取狀態值 …                    }                     finally                     {                        rwLock.ExitReadLock();                    }                }            }             finally             {                if (isUpdated)                    rwLock.ExitUpgradeableReadLock();            }        }    }}
ReaderWriterLockSlim 的遞迴策略

新的讀寫鎖還有一個有意思的特性就是它的遞迴策略。預設情況下,除已提及的降級到讀鎖和升級到寫鎖之外,所有的遞迴請求都不允許。這意味著你不能連續兩次調用 EnterReadLock,其他模式下也類似。如果你這麼做,CLR 將會拋出 LockRecursionException 異常。當然,你可以使用 LockRecursionPolicy.SupportsRecursion 的建構函式參數讓該讀寫鎖支援遞迴鎖定。但不建議對新的開發使用遞迴,因為遞迴會帶來不必要的複雜情況,從而使你的代碼更容易出現死結現象。

有一種特殊的情況永遠也不被允許,無論你採取什麼樣的遞迴策略。這就是當線程持有讀鎖時請求寫鎖。Microsoft 曾經考慮提供這樣的支援,但是這種情況太容易導致死結。所以 Microsoft 最終放棄了這個方案。

此外,這個新的讀寫鎖還提供了很多對應的屬性來確定線程是否在指定模型下持有該鎖。比如 IsReadLockHeld, IsWriteLockHeld 和 IsUpgradeableReadLockHeld 。你也可以通過 WaitingReadCount,WaitingWriteCount 和 WaitingUpgradeCount 等屬性來查看有多少線程正在等待持有特定模式下的鎖。CurrentReadCount 屬性則告知目前有多少並發讀線程。RecursiveReadCount, RecursiveWriteCount 和 RecursiveUpgradeCount 則告知目前線程進入特定模式鎖定狀態下的次數。

小結

這篇文章分析了 .NET 中提供的兩個讀寫鎖類。然而 .NET 3.5 提供的新讀寫鎖 ReaderWriterLockSlim 類消除了 ReaderWriterLock 類存在的主要問題。與 ReaderWriterLock 相比,效能有了極大提高。更新具有原子性,也可以極大避免死結。更有清晰的遞迴策略。在任何情況下,我們都應該使用 ReaderWriterLockSlim 來代替 ReaderWriterLock 類。

Update 於 2008-12-07 0:06

Windows Vista 及其以後的版本新增了一個 SRWLock 原語。它以 Windows 核心事件機製為基礎而構建。它的設計比較有意思。

SRW 鎖不支援遞迴。Windows Kernel 團隊認為支援遞迴會造成額外系統開銷,原因是為了維持準確性需進行逐線程的計數。SRW 鎖也不支援從共用訪問升級到獨佔訪問,同時也不支援從獨佔訪問降級到共用訪問。支援升級能力可能會造成難以接受的複雜性和額外系統開銷,這種開銷甚至會影響鎖內共用和獨佔獲得代碼的常見情況。它還要求定義關於如何選擇等待中的讀取器、等待中的寫入器和等待升級的讀取器的策略,這又將與無偏向的基本設計目標相抵觸。我對其進行了 .NET 封裝。代碼如下:

using System;using System.Threading;using System.Runtime.InteropServices;namespace Lucifer.Threading.Lock{    /// <summary>    /// Windows NT 6.0 才支援的讀寫鎖。    /// </summary>    /// <remarks>請注意,這個類只能在 NT 6.0 及以後的版本中才能使用。</remarks>    public sealed class SRWLock    {        private IntPtr rwLock;        /// <summary>        /// 該鎖不支援遞迴。        /// </summary>        public SRWLock()        {            InitializeSRWLock(out rwLock);        }        /// <summary>        /// 獲得讀鎖。        /// </summary>        public void EnterReadLock()        {            AcquireSRWLockShared(ref rwLock);        }        /// <summary>        /// 獲得寫鎖。        /// </summary>        public void EnterWriteLock()        {            AcquireSRWLockExclusive(ref rwLock);        }        /// <summary>        /// 釋放讀鎖。        /// </summary>        public void ExitReadLock()        {            ReleaseSRWLockShared(ref rwLock);        }        /// <summary>        /// 釋放寫鎖。        /// </summary>        public void ExitWriteLock()        {            ReleaseSRWLockExclusive(ref rwLock);        }        [DllImport("Kernel32", CallingConvention = CallingConvention.Winapi, ExactSpelling = true)]        private static extern void InitializeSRWLock(out IntPtr rwLock);        [DllImport("Kernel32", CallingConvention = CallingConvention.Winapi, ExactSpelling = true)]        private static extern void AcquireSRWLockExclusive(ref IntPtr rwLock);        [DllImport("Kernel32", CallingConvention = CallingConvention.Winapi, ExactSpelling = true)]        private static extern void AcquireSRWLockShared(ref IntPtr rwLock);        [DllImport("Kernel32", CallingConvention = CallingConvention.Winapi, ExactSpelling = true)]        private static extern void ReleaseSRWLockExclusive(ref IntPtr rwLock);        [DllImport("Kernel32", CallingConvention = CallingConvention.Winapi, ExactSpelling = true)]        private static extern void ReleaseSRWLockShared(ref IntPtr rwLock);     }}

此外,在其他平台也有一些有意思的讀寫鎖。比如 Linux 核心中的讀寫鎖和 Java 中的讀寫鎖。感興趣的同學可以自己研究一番。

讀寫鎖有個很常用的情境就是在緩衝設計中。因為緩衝中經常有些很穩定,不太長更新的內容。MSDN 的程式碼範例就很經典,我原版拷貝一下,呵呵。程式碼範例如下:

using System;using System.Threading;namespace Lucifer.CSharp.Sample{    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 void Delete(int key)        {            cacheLock.EnterWriteLock();            try            {                innerCache.Remove(key);            }            finally            {                cacheLock.ExitWriteLock();            }        }        public enum AddOrUpdateStatus        {            Added,            Updated,            Unchanged        };    }}
再次 Update 於 2008-12-07 0:47

如果應用情境要求效能十分苛刻,可以考慮採用 lock-free 方案。但是 lock-free 有著固有缺陷:極難編碼,極難證明其正確性。讀寫鎖方案的應用範圍更加廣泛一些。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.