一、lock 、Monitor
處理並行任務的時候,效率最高的就是多線程。當不同線程需要訪問同一資源時候,就需要同步了。就像生活中很多人要一起趕飛機大家都要訪問飛機這個資源每個人是一條線程那么就有門,有了門就代表每次只能一位其他人都要排隊進入。
Monitor 類控制對對象的訪問通過授予為單線程的對象的鎖。對象鎖提供的功能來限制對代碼通常稱為關鍵節的塊的訪問。一個線程擁有對象的鎖,而沒有其他線程可以獲取該鎖。您還可以使用 Monitor 類,以確保沒有其他線程有權訪問的應用程序一部分的代碼正在執行的鎖的所有者,除非其他線程正在執行使用不同的鎖定的對象的代碼。
lock(object obj) //監視obj 是否有被使用或者lock,如果沒有就有我使用,否則一直等待obj被釋放。 { ...... }//釋放obj lock(this) //鎖住當前對象 將會鎖住當前類的實例 不能在靜態資源中使用 { ...... }//釋放this
lock 就是下面代碼的語法糖
try
{ Monitor.Enter(this); XXX你的代碼 } finally { Monitor.Exit(this); }
demo
using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; public class Example { public static void Main() { List<Task> tasks = new List<Task>(); Random rnd = new Random(); long total = 0; int n = 0; for (int taskCtr = 0; taskCtr < 10; taskCtr++) tasks.Add(Task.Run( () => { int[] values = new int[10000]; int taskTotal = 0; int taskN = 0; int ctr = 0; Monitor.Enter(rnd); // Generate 10,000 random integers for (ctr = 0; ctr < 10000; ctr++) values[ctr] = rnd.Next(0, 1001); Monitor.Exit(rnd); taskN = ctr; foreach (var value in values) taskTotal += value; Console.WriteLine("Mean for task {0,2}: {1:N2} (N={2:N0})", Task.CurrentId, (taskTotal * 1.0)/taskN, taskN); Interlocked.Add(ref n, taskN); Interlocked.Add(ref total, taskTotal); } )); try { Task.WaitAll(tasks.ToArray()); Console.WriteLine("\nMean for all tasks: {0:N2} (N={1:N0})", (total * 1.0)/n, n); } catch (AggregateException e) { foreach (var ie in e.InnerExceptions) Console.WriteLine("{0}: {1}", ie.GetType().Name, ie.Message); } } } // The example displays output like the following: // Mean for task 1: 499.04 (N=10,000) // Mean for task 2: 500.42 (N=10,000) // Mean for task 3: 499.65 (N=10,000) // Mean for task 8: 502.59 (N=10,000) // Mean for task 5: 502.75 (N=10,000) // Mean for task 4: 494.88 (N=10,000) // Mean for task 7: 499.22 (N=10,000) // Mean for task 10: 496.45 (N=10,000) // Mean for task 6: 499.75 (N=10,000) // Mean for task 9: 502.79 (N=10,000) // // Mean for all tasks: 499.75 (N=100,000)
二、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,但是由於某些原因,這個類還有存在的必要。
Reader-Reader,第二個不需等待,直接獲得讀控制權;
Reader-Writer,第二個需要等待第一個調用釋放讀控制權后,才能獲得寫控制權;
Writer-Writer,第二個需要等待第一個調用釋放寫控制權后,才能獲得寫控制權;
Writer-Reader,第二個需要等待第一個調用釋放寫控制權后,才能獲得讀控制權。
| 重要事項 |
|---|
| .NET Framework 具有兩個讀取器 / 編寫器鎖, ReaderWriterLockSlim 和 ReaderWriterLock。 ReaderWriterLockSlim 建議將所有新的開發的。 ReaderWriterLockSlim 類似於 ReaderWriterLock, ,只是簡化了遞歸、 升級和降級鎖定狀態的規則。 ReaderWriterLockSlim 可避免潛在的死鎖的很多情況。 此外,性能的 ReaderWriterLockSlim 明顯優於 ReaderWriterLock。 |
三、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); } }
讀寫鎖有個很常用的場景就是在緩存設計中。因為緩存中經常有些很穩定,不太長更新的內容。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 }; } }
摘抄轉載方便自己記憶免得到處找
原文:http://blog.csdn.net/zengjibing/archive/2009/02/22/3923168.aspx
