C#多线程编程系列(三)- 线程同步
目錄
- 1.1 簡(jiǎn)介
- 1.2 執(zhí)行基本原子操作
- 1.3 使用Mutex類(lèi)
- 1.4 使用SemaphoreSlim類(lèi)
- 1.5 使用AutoResetEvent類(lèi)
- 1.6 使用ManualResetEventSlim類(lèi)
- 1.7 使用CountDownEvent類(lèi)
- 1.8 使用Barrier類(lèi)
- 1.9 使用ReaderWriterLockSlim類(lèi)
- 1.10 使用SpinWait類(lèi)
- 參考書(shū)籍
- 筆者水平有限,如果錯(cuò)誤歡迎各位批評(píng)指正!
1.1 簡(jiǎn)介#
本章介紹在C#中實(shí)現(xiàn)線程同步的幾種方法。因?yàn)槎鄠€(gè)線程同時(shí)訪問(wèn)共享數(shù)據(jù)時(shí),可能會(huì)造成共享數(shù)據(jù)的損壞,從而導(dǎo)致與預(yù)期的結(jié)果不相符。為了解決這個(gè)問(wèn)題,所以需要用到線程同步,也被俗稱(chēng)為“加鎖”。但是加鎖絕對(duì)不對(duì)提高性能,最多也就是不增不減,要實(shí)現(xiàn)性能不增不減還得靠高質(zhì)量的同步源語(yǔ)(Synchronization Primitive)。但是因?yàn)?strong>正確永遠(yuǎn)比速度更重要,所以線程同步在某些場(chǎng)景下是必須的。
線程同步有兩種源語(yǔ)(Primitive)構(gòu)造:用戶(hù)模式(user - mode)和內(nèi)核模式(kernel - mode),當(dāng)資源可用時(shí)間短的情況下,用戶(hù)模式要優(yōu)于內(nèi)核模式,但是如果長(zhǎng)時(shí)間不能獲得資源,或者說(shuō)長(zhǎng)時(shí)間處于“自旋”,那么內(nèi)核模式是相對(duì)來(lái)說(shuō)好的選擇。
但是我們希望兼具用戶(hù)模式和內(nèi)核模式的優(yōu)點(diǎn),我們把它稱(chēng)為混合構(gòu)造(hybrid construct),它兼具了兩種模式的優(yōu)點(diǎn)。
在C#中有多種線程同步的機(jī)制,通常可以按照以下順序進(jìn)行選擇。
在同步中,一定要注意避免死鎖的發(fā)生,死鎖的發(fā)生必須滿足以下4個(gè)基本條件,所以只需要破壞任意一個(gè)條件,就可避免發(fā)生死鎖。
1.2 執(zhí)行基本原子操作#
CLR保證了對(duì)這些數(shù)據(jù)類(lèi)型的讀寫(xiě)是原子性的:Boolean、Char、(S)Byte、(U)Int16、(U)Int32、(U)IntPtr和Single。但是如果讀寫(xiě)Int64可能會(huì)發(fā)生讀取撕裂(torn read)的問(wèn)題,因?yàn)樵?2位操作系統(tǒng)中,它需要執(zhí)行兩次Mov操作,無(wú)法在一個(gè)時(shí)間內(nèi)執(zhí)行完成。
那么在本節(jié)中,就會(huì)著重的介紹System.Threading.Interlocked類(lèi)提供的方法,Interlocked類(lèi)中的每個(gè)方法都是執(zhí)行一次的讀取以及寫(xiě)入操作。更多與Interlocked類(lèi)相關(guān)的資料請(qǐng)參考鏈接,戳一戳本文不在贅述。
演示代碼如下所示,分別使用了三種方式進(jìn)行計(jì)數(shù):錯(cuò)誤計(jì)數(shù)方式、lock鎖方式和Interlocked原子方式。
Copy
private static void Main(string[] args) { Console.WriteLine("錯(cuò)誤的計(jì)數(shù)"); var c = new Counter(); Execute(c); Console.WriteLine("--------------------------"); Console.WriteLine("正確的計(jì)數(shù) - 有鎖"); var c2 = new CounterWithLock(); Execute(c2); Console.WriteLine("--------------------------"); Console.WriteLine("正確的計(jì)數(shù) - 無(wú)鎖"); var c3 = new CounterNoLock(); Execute(c3); Console.ReadLine(); } static void Execute(CounterBase c) { // 統(tǒng)計(jì)耗時(shí) var sw = new Stopwatch(); sw.Start(); var t1 = new Thread(() => TestCounter(c)); var t2 = new Thread(() => TestCounter(c)); var t3 = new Thread(() => TestCounter(c)); t1.Start(); t2.Start(); t3.Start(); t1.Join(); t2.Join(); t3.Join(); sw.Stop(); Console.WriteLine($"Total count: {c.Count} Time:{sw.ElapsedMilliseconds} ms"); } static void TestCounter(CounterBase c) { for (int i = 0; i < 100000; i++) { c.Increment(); c.Decrement(); } } class Counter : CounterBase { public override void Increment() { _count++; } public override void Decrement() { _count--; } } class CounterNoLock : CounterBase { public override void Increment() { // 使用Interlocked執(zhí)行原子操作 Interlocked.Increment(ref _count); } public override void Decrement() { Interlocked.Decrement(ref _count); } } class CounterWithLock : CounterBase { private readonly object _syncRoot = new Object(); public override void Increment() { // 使用Lock關(guān)鍵字 鎖定私有變量 lock (_syncRoot) { // 同步塊 Count++; } } public override void Decrement() { lock (_syncRoot) { Count--; } } } abstract class CounterBase { protected int _count; public int Count { get { return _count; } set { _count = value; } } public abstract void Increment(); public abstract void Decrement(); }
運(yùn)行結(jié)果如下所示,與預(yù)期結(jié)果基本相符。
1.3 使用Mutex類(lèi)#
System.Threading.Mutex在概念上和System.Threading.Monitor幾乎一樣,但是Mutex同步對(duì)文件或者其他跨進(jìn)程的資源進(jìn)行訪問(wèn),也就是說(shuō)Mutex是可跨進(jìn)程的。因?yàn)槠涮匦?#xff0c;它的一個(gè)用途是限制應(yīng)用程序不能同時(shí)運(yùn)行多個(gè)實(shí)例。
Mutex對(duì)象支持遞歸,也就是說(shuō)同一個(gè)線程可多次獲取同一個(gè)鎖,這在后面演示代碼中可觀察到。由于Mutex的基類(lèi)System.Theading.WaitHandle實(shí)現(xiàn)了IDisposable接口,所以當(dāng)不需要在使用它時(shí)要注意進(jìn)行資源的釋放。更多資料:戳一戳
演示代碼如下所示,簡(jiǎn)單的演示了如何創(chuàng)建單實(shí)例的應(yīng)用程序和Mutex遞歸獲取鎖的實(shí)現(xiàn)。
Copy
const string MutexName = "CSharpThreadingCookbook"; static void Main(string[] args) { // 使用using 及時(shí)釋放資源 using (var m = new Mutex(false, MutexName)) { if (!m.WaitOne(TimeSpan.FromSeconds(5), false)) { Console.WriteLine("已經(jīng)有實(shí)例正在運(yùn)行!"); } else { Console.WriteLine("運(yùn)行中..."); // 演示遞歸獲取鎖 Recursion(); Console.ReadLine(); m.ReleaseMutex(); } } Console.ReadLine(); } static void Recursion() { using (var m = new Mutex(false, MutexName)) { if (!m.WaitOne(TimeSpan.FromSeconds(2), false)) { // 因?yàn)镸utex支持遞歸獲取鎖 所以永遠(yuǎn)不會(huì)執(zhí)行到這里 Console.WriteLine("遞歸獲取鎖失敗!"); } else { Console.WriteLine("遞歸獲取鎖成功!"); } } }
運(yùn)行結(jié)果如下圖所示,打開(kāi)了兩個(gè)應(yīng)用程序,因?yàn)槭褂肕utex實(shí)現(xiàn)了單實(shí)例,所以第二個(gè)應(yīng)用程序無(wú)法獲取鎖,就會(huì)顯示已有實(shí)例正在運(yùn)行。
1.4 使用SemaphoreSlim類(lèi)#
SemaphoreSlim類(lèi)與之前提到的同步類(lèi)有鎖不同,之前提到的同步類(lèi)都是互斥的,也就是說(shuō)只允許一個(gè)線程進(jìn)行訪問(wèn)資源,而SemaphoreSlim是可以允許多個(gè)訪問(wèn)。
在之前的部分有提到,以*Slim結(jié)尾的線程同步類(lèi),都是工作在混合模式下的,也就是說(shuō)開(kāi)始它們都是在用戶(hù)模式下"自旋",等發(fā)生第一次競(jìng)爭(zhēng)時(shí),才切換到內(nèi)核模式。但是SemaphoreSlim不同于Semaphore類(lèi),它不支持系統(tǒng)信號(hào)量,所以它不能用于進(jìn)程之間的同步。
該類(lèi)使用比較簡(jiǎn)單,演示代碼演示了6個(gè)線程競(jìng)爭(zhēng)訪問(wèn)只允許4個(gè)線程同時(shí)訪問(wèn)的數(shù)據(jù)庫(kù),如下所示。
Copy
static void Main(string[] args) { // 創(chuàng)建6個(gè)線程 競(jìng)爭(zhēng)訪問(wèn)AccessDatabase for (int i = 1; i <= 6; i++) { string threadName = "線程 " + i; // 越后面的線程,訪問(wèn)時(shí)間越久 方便查看效果 int secondsToWait = 2 + 2 * i; var t = new Thread(() => AccessDatabase(threadName, secondsToWait)); t.Start(); } Console.ReadLine(); } // 同時(shí)允許4個(gè)線程訪問(wèn) static SemaphoreSlim _semaphore = new SemaphoreSlim(4); static void AccessDatabase(string name, int seconds) { Console.WriteLine($"{name} 等待訪問(wèn)數(shù)據(jù)庫(kù).... {DateTime.Now.ToString("HH:mm:ss.ffff")}"); // 等待獲取鎖 進(jìn)入臨界區(qū) _semaphore.Wait(); Console.WriteLine($"{name} 已獲取對(duì)數(shù)據(jù)庫(kù)的訪問(wèn)權(quán)限 {DateTime.Now.ToString("HH:mm:ss.ffff")}"); // Do something Thread.Sleep(TimeSpan.FromSeconds(seconds)); Console.WriteLine($"{name} 訪問(wèn)完成... {DateTime.Now.ToString("HH:mm:ss.ffff")}"); // 釋放鎖 _semaphore.Release(); }
運(yùn)行結(jié)果如下所示,可見(jiàn)前4個(gè)線程馬上就獲取到了鎖,進(jìn)入了臨界區(qū),而另外兩個(gè)線程在等待;等有鎖被釋放時(shí),才能進(jìn)入臨界區(qū)。
1.5 使用AutoResetEvent類(lèi)#
AutoResetEvent叫自動(dòng)重置事件,雖然名稱(chēng)中有事件一詞,但是重置事件和C#中的委托沒(méi)有任何關(guān)系,這里的事件只是由內(nèi)核維護(hù)的Boolean變量,當(dāng)事件為false,那么在事件上等待的線程就阻塞;事件變?yōu)閠rue,那么阻塞解除。
在.Net中有兩種此類(lèi)事件,即AutoResetEvent(自動(dòng)重置事件)和ManualResetEvent(手動(dòng)重置事件)。這兩者均是采用內(nèi)核模式,它的區(qū)別在于當(dāng)重置事件為true時(shí),自動(dòng)重置事件它只喚醒一個(gè)阻塞的線程,會(huì)自動(dòng)將事件重置回false,造成其它線程繼續(xù)阻塞。而手動(dòng)重置事件不會(huì)自動(dòng)重置,必須通過(guò)代碼手動(dòng)重置回false。
因?yàn)橐陨系脑?#xff0c;所以在很多文章和書(shū)籍中不推薦使用AutoResetEvent(自動(dòng)重置事件),因?yàn)樗苋菀自诰帉?xiě)生產(chǎn)者線程時(shí)發(fā)生失誤,造成它的迭代次數(shù)多余消費(fèi)者線程。
演示代碼如下所示,該代碼演示了通過(guò)AutoResetEvent實(shí)現(xiàn)兩個(gè)線程的互相同步。
Copy
static void Main(string[] args) { var t = new Thread(() => Process(10)); t.Start(); Console.WriteLine("等待另一個(gè)線程完成工作!"); // 等待工作線程通知 主線程阻塞 _workerEvent.WaitOne(); Console.WriteLine("第一個(gè)操作已經(jīng)完成!"); Console.WriteLine("在主線程上執(zhí)行操作"); Thread.Sleep(TimeSpan.FromSeconds(5)); // 發(fā)送通知 工作線程繼續(xù)運(yùn)行 _mainEvent.Set(); Console.WriteLine("現(xiàn)在在第二個(gè)線程上運(yùn)行第二個(gè)操作"); // 等待工作線程通知 主線程阻塞 _workerEvent.WaitOne(); Console.WriteLine("第二次操作完成!"); Console.ReadLine(); } // 工作線程Event private static AutoResetEvent _workerEvent = new AutoResetEvent(false); // 主線程Event private static AutoResetEvent _mainEvent = new AutoResetEvent(false); static void Process(int seconds) { Console.WriteLine("開(kāi)始長(zhǎng)時(shí)間的工作..."); Thread.Sleep(TimeSpan.FromSeconds(seconds)); Console.WriteLine("工作完成!"); // 發(fā)送通知 主線程繼續(xù)運(yùn)行 _workerEvent.Set(); Console.WriteLine("等待主線程完成其它工作"); // 等待主線程通知 工作線程阻塞 _mainEvent.WaitOne(); Console.WriteLine("啟動(dòng)第二次操作..."); Thread.Sleep(TimeSpan.FromSeconds(seconds)); Console.WriteLine("工作完成!"); // 發(fā)送通知 主線程繼續(xù)運(yùn)行 _workerEvent.Set(); }
運(yùn)行結(jié)果如下圖所示,與預(yù)期結(jié)果符合。
1.6 使用ManualResetEventSlim類(lèi)#
ManualResetEventSlim使用和ManualResetEvent類(lèi)基本一致,只是ManualResetEventSlim工作在混合模式下,而它與AutoResetEventSlim不同的地方就是需要手動(dòng)重置事件,也就是調(diào)用Reset()才能將事件重置為false。
演示代碼如下,形象的將ManualResetEventSlim比喻成大門(mén),當(dāng)事件為true時(shí)大門(mén)打開(kāi),線程解除阻塞;而事件為false時(shí)大門(mén)關(guān)閉,線程阻塞。
Copy
static void Main(string[] args) { var t1 = new Thread(() => TravelThroughGates("Thread 1", 5)); var t2 = new Thread(() => TravelThroughGates("Thread 2", 6)); var t3 = new Thread(() => TravelThroughGates("Thread 3", 12)); t1.Start(); t2.Start(); t3.Start(); // 休眠6秒鐘 只有Thread 1小于 6秒鐘,所以事件重置時(shí) Thread 1 肯定能進(jìn)入大門(mén) 而 Thread 2 可能可以進(jìn)入大門(mén) Thread.Sleep(TimeSpan.FromSeconds(6)); Console.WriteLine($"大門(mén)現(xiàn)在打開(kāi)了! 時(shí)間:{DateTime.Now.ToString("mm:ss.ffff")}"); _mainEvent.Set(); // 休眠2秒鐘 此時(shí) Thread 2 肯定可以進(jìn)入大門(mén) Thread.Sleep(TimeSpan.FromSeconds(2)); _mainEvent.Reset(); Console.WriteLine($"大門(mén)現(xiàn)在關(guān)閉了! 時(shí)間:{DateTime.Now.ToString("mm: ss.ffff")}"); // 休眠10秒鐘 Thread 3 可以進(jìn)入大門(mén) Thread.Sleep(TimeSpan.FromSeconds(10)); Console.WriteLine($"大門(mén)現(xiàn)在第二次打開(kāi)! 時(shí)間:{DateTime.Now.ToString("mm: ss.ffff")}"); _mainEvent.Set(); Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine($"大門(mén)現(xiàn)在關(guān)閉了! 時(shí)間:{DateTime.Now.ToString("mm: ss.ffff")}"); _mainEvent.Reset(); Console.ReadLine(); } static void TravelThroughGates(string threadName, int seconds) { Console.WriteLine($"{threadName} 進(jìn)入睡眠 時(shí)間:{DateTime.Now.ToString("mm:ss.ffff")}"); Thread.Sleep(TimeSpan.FromSeconds(seconds)); Console.WriteLine($"{threadName} 等待大門(mén)打開(kāi)! 時(shí)間:{DateTime.Now.ToString("mm:ss.ffff")}"); _mainEvent.Wait(); Console.WriteLine($"{threadName} 進(jìn)入大門(mén)! 時(shí)間:{DateTime.Now.ToString("mm:ss.ffff")}"); } static ManualResetEventSlim _mainEvent = new ManualResetEventSlim(false);
運(yùn)行結(jié)果如下,與預(yù)期結(jié)果相符。
1.7 使用CountDownEvent類(lèi)#
CountDownEvent類(lèi)內(nèi)部構(gòu)造使用了一個(gè)ManualResetEventSlim對(duì)象。這個(gè)構(gòu)造阻塞一個(gè)線程,直到它內(nèi)部計(jì)數(shù)器(CurrentCount)變?yōu)?時(shí),才解除阻塞。也就是說(shuō)它并不是阻止對(duì)已經(jīng)枯竭的資源池的訪問(wèn),而是只有當(dāng)計(jì)數(shù)為0時(shí)才允許訪問(wèn)。
這里需要注意的是,當(dāng)CurrentCount變?yōu)?時(shí),那么它就不能被更改了。為0以后,Wait()方法的阻塞被解除。
演示代碼如下所示,只有當(dāng)Signal()方法被調(diào)用2次以后,Wait()方法的阻塞才被解除。
Copy
static void Main(string[] args) { Console.WriteLine($"開(kāi)始兩個(gè)操作 {DateTime.Now.ToString("mm:ss.ffff")}"); var t1 = new Thread(() => PerformOperation("操作 1 完成!", 4)); var t2 = new Thread(() => PerformOperation("操作 2 完成!", 8)); t1.Start(); t2.Start(); // 等待操作完成 _countdown.Wait(); Console.WriteLine($"所有操作都完成 {DateTime.Now.ToString("mm: ss.ffff")}"); _countdown.Dispose(); Console.ReadLine(); } // 構(gòu)造函數(shù)的參數(shù)為2 表示只有調(diào)用了兩次 Signal方法 CurrentCount 為 0時(shí) Wait的阻塞才解除 static CountdownEvent _countdown = new CountdownEvent(2); static void PerformOperation(string message, int seconds) { Thread.Sleep(TimeSpan.FromSeconds(seconds)); Console.WriteLine($"{message} {DateTime.Now.ToString("mm:ss.ffff")}"); // CurrentCount 遞減 1 _countdown.Signal(); }
運(yùn)行結(jié)果如下圖所示,可見(jiàn)只有當(dāng)操作1和操作2都完成以后,才執(zhí)行輸出所有操作都完成。
1.8 使用Barrier類(lèi)#
Barrier類(lèi)用于解決一個(gè)非常稀有的問(wèn)題,平時(shí)一般用不上。Barrier類(lèi)控制一系列線程進(jìn)行階段性的并行工作。
假設(shè)現(xiàn)在并行工作分為2個(gè)階段,每個(gè)線程在完成它自己那部分階段1的工作后,必須停下來(lái)等待其它線程完成階段1的工作;等所有線程均完成階段1工作后,每個(gè)線程又開(kāi)始運(yùn)行,完成階段2工作,等待其它線程全部完成階段2工作后,整個(gè)流程才結(jié)束。
演示代碼如下所示,該代碼演示了兩個(gè)線程分階段的完成工作。
Copy
static void Main(string[] args) { var t1 = new Thread(() => PlayMusic("鋼琴家", "演奏一首令人驚嘆的獨(dú)奏曲", 5)); var t2 = new Thread(() => PlayMusic("歌手", "唱著他的歌", 2)); t1.Start(); t2.Start(); Console.ReadLine(); } static Barrier _barrier = new Barrier(2, Console.WriteLine($"第 {b.CurrentPhaseNumber + 1} 階段結(jié)束")); static void PlayMusic(string name, string message, int seconds) { for (int i = 1; i < 3; i++) { Console.WriteLine("----------------------------------------------"); Thread.Sleep(TimeSpan.FromSeconds(seconds)); Console.WriteLine($"{name} 開(kāi)始 {message}"); Thread.Sleep(TimeSpan.FromSeconds(seconds)); Console.WriteLine($"{name} 結(jié)束 {message}"); _barrier.SignalAndWait(); } }
運(yùn)行結(jié)果如下所示,當(dāng)“歌手”線程完成后,并沒(méi)有馬上結(jié)束,而是等待“鋼琴家”線程結(jié)束,當(dāng)"鋼琴家"線程結(jié)束后,才開(kāi)始第2階段的工作。
1.9 使用ReaderWriterLockSlim類(lèi)#
ReaderWriterLockSlim類(lèi)主要是解決在某些場(chǎng)景下,讀操作多于寫(xiě)操作而使用某些互斥鎖當(dāng)多個(gè)線程同時(shí)訪問(wèn)資源時(shí),只有一個(gè)線程能訪問(wèn),導(dǎo)致性能急劇下降。
如果所有線程都希望以只讀的方式訪問(wèn)數(shù)據(jù),就根本沒(méi)有必要阻塞它們;如果一個(gè)線程希望修改數(shù)據(jù),那么這個(gè)線程才需要獨(dú)占訪問(wèn),這就是ReaderWriterLockSlim的典型應(yīng)用場(chǎng)景。這個(gè)類(lèi)就像下面這樣來(lái)控制線程。
- 一個(gè)線程向數(shù)據(jù)寫(xiě)入是,請(qǐng)求訪問(wèn)的其他所有線程都被阻塞。
- 一個(gè)線程讀取數(shù)據(jù)時(shí),請(qǐng)求讀取的線程允許讀取,而請(qǐng)求寫(xiě)入的線程被阻塞。
- 寫(xiě)入線程結(jié)束后,要么解除一個(gè)寫(xiě)入線程的阻塞,使寫(xiě)入線程能向數(shù)據(jù)接入,要么解除所有讀取線程的阻塞,使它們能并發(fā)讀取數(shù)據(jù)。如果線程沒(méi)有被阻塞,鎖就可以進(jìn)入自由使用的狀態(tài),可供下一個(gè)讀線程或?qū)懢€程獲取。
- 從數(shù)據(jù)讀取的所有線程結(jié)束后,一個(gè)寫(xiě)線程被解除阻塞,使它能向數(shù)據(jù)寫(xiě)入。如果線程沒(méi)有被阻塞,鎖就可以進(jìn)入自由使用的狀態(tài),可供下一個(gè)讀線程或?qū)懢€程獲取。
ReaderWriterLockSlim還支持從讀線程升級(jí)為寫(xiě)線程的操作,詳情請(qǐng)戳一戳。文本不作介紹。ReaderWriterLock類(lèi)已經(jīng)過(guò)時(shí),而且存在許多問(wèn)題,沒(méi)有必要去使用。
示例代碼如下所示,創(chuàng)建了3個(gè)讀線程,2個(gè)寫(xiě)線程,讀線程和寫(xiě)線程競(jìng)爭(zhēng)獲取鎖。
Copy
static void Main(string[] args) { // 創(chuàng)建3個(gè) 讀線程 new Thread(() => Read("Reader 1")) { IsBackground = true }.Start(); new Thread(() => Read("Reader 2")) { IsBackground = true }.Start(); new Thread(() => Read("Reader 3")) { IsBackground = true }.Start(); // 創(chuàng)建兩個(gè)寫(xiě)線程 new Thread(() => Write("Writer 1")) { IsBackground = true }.Start(); new Thread(() => Write("Writer 2")) { IsBackground = true }.Start(); // 使程序運(yùn)行30S Thread.Sleep(TimeSpan.FromSeconds(30)); Console.ReadLine(); } static ReaderWriterLockSlim _rw = new ReaderWriterLockSlim(); static Dictionary<int, int> _items = new Dictionary<int, int>(); static void Read(string threadName) { while (true) { try { // 獲取讀鎖定 _rw.EnterReadLock(); Console.WriteLine($"{threadName} 從字典中讀取內(nèi)容 {DateTime.Now.ToString("mm:ss.ffff")}"); foreach (var key in _items.Keys) { Thread.Sleep(TimeSpan.FromSeconds(0.1)); } } finally { // 釋放讀鎖定 _rw.ExitReadLock(); } } } static void Write(string threadName) { while (true) { try { int newKey = new Random().Next(250); // 嘗試進(jìn)入可升級(jí)鎖模式狀態(tài) _rw.EnterUpgradeableReadLock(); if (!_items.ContainsKey(newKey)) { try { // 獲取寫(xiě)鎖定 _rw.EnterWriteLock(); _items[newKey] = 1; Console.WriteLine($"{threadName} 將新的鍵 {newKey} 添加進(jìn)入字典中 {DateTime.Now.ToString("mm:ss.ffff")}"); } finally { // 釋放寫(xiě)鎖定 _rw.ExitWriteLock(); } } Thread.Sleep(TimeSpan.FromSeconds(0.1)); } finally { // 減少可升級(jí)模式遞歸計(jì)數(shù),并在計(jì)數(shù)為0時(shí) 推出可升級(jí)模式 _rw.ExitUpgradeableReadLock(); } } }
運(yùn)行結(jié)果如下所示,與預(yù)期結(jié)果相符。
1.10 使用SpinWait類(lèi)#
SpinWait是一個(gè)常用的混合模式的類(lèi),它被設(shè)計(jì)成使用用戶(hù)模式等待一段時(shí)間,人后切換至內(nèi)核模式以節(jié)省CPU時(shí)間。
它的使用非常簡(jiǎn)單,演示代碼如下所示。
Copy
static void Main(string[] args) { var t1 = new Thread(UserModeWait); var t2 = new Thread(HybridSpinWait); Console.WriteLine("運(yùn)行在用戶(hù)模式下"); t1.Start(); Thread.Sleep(20); _isCompleted = true; Thread.Sleep(TimeSpan.FromSeconds(1)); _isCompleted = false; Console.WriteLine("運(yùn)行在混合模式下"); t2.Start(); Thread.Sleep(5); _isCompleted = true; Console.ReadLine(); } static volatile bool _isCompleted = false; static void UserModeWait() { while (!_isCompleted) { Console.Write("."); } Console.WriteLine(); Console.WriteLine("等待結(jié)束"); } static void HybridSpinWait() { var w = new SpinWait(); while (!_isCompleted) { w.SpinOnce(); Console.WriteLine(w.NextSpinWillYield); } Console.WriteLine("等待結(jié)束"); }
運(yùn)行結(jié)果如下兩圖所示,首先程序運(yùn)行在模擬的用戶(hù)模式下,使CPU有一個(gè)短暫的峰值。然后使用SpinWait工作在混合模式下,首先標(biāo)志變量為False處于用戶(hù)模式自旋中,等待以后進(jìn)入內(nèi)核模式。
參考書(shū)籍
本文主要參考了以下幾本書(shū),在此對(duì)這些作者表示由衷的感謝你們提供了這么好的資料。
源碼下載點(diǎn)擊鏈接?示例源碼下載
筆者水平有限,如果錯(cuò)誤歡迎各位批評(píng)指正!
作者:InCerry
出處:https://www.cnblogs.com/InCerry/p/9416382.html
版權(quán):本文采用「署名 4.0 國(guó)際」知識(shí)共享許可協(xié)議進(jìn)行許可。
總結(jié)
以上是生活随笔為你收集整理的C#多线程编程系列(三)- 线程同步的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 百度网盘SVIP直降100元 买就送61
- 下一篇: C#线程模型脉络