C#多線程系列之線程等待
前言
volatile 關(guān)鍵字
volatile
關(guān)鍵字指示一個字段可以由多個同時執(zhí)行的線程修改。
我們繼續(xù)使用《C#多線程(3):原子操作》中的示例:
static void Main(string[] args) { for (int i = 0; i < 5; i++) { new Thread(AddOne).Start(); } Thread.Sleep(TimeSpan.FromSeconds(5)); Console.WriteLine("sum = " + sum); Console.ReadKey(); } private static int sum = 0; public static void AddOne() { for (int i = 0; i < 100_0000; i++) { sum += 1; } }
運(yùn)行后你會發(fā)現(xiàn),結(jié)果不為 500_0000,而使用 Interlocked.Increment(ref sum);
后,可以獲得準(zhǔn)確可靠的結(jié)果。
你試試再運(yùn)行下面的示例:
static void Main(string[] args) { for (int i = 0; i < 5; i++) { new Thread(AddOne).Start(); } Thread.Sleep(TimeSpan.FromSeconds(5)); Console.WriteLine("sum = " + sum); Console.ReadKey(); } private static volatile int sum = 0; public static void AddOne() { for (int i = 0; i < 100_0000; i++) { sum += 1; } }
你以為正常了?哈哈哈,并沒有。
volatile 的作用在于讀,保證了觀察的順序和寫入的順序一致,每次讀取的都是最新的一個值;不會干擾寫操作。
詳情請點(diǎn)擊:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/volatile
其原理解釋:https://theburningmonk.com/2010/03/threading-understanding-the-volatile-modifier-in-csharp/
三種常用等待
這三種等待分別是:
Thread.Sleep();
Thread.SpinWait();
Task.Delay();
Thread.Sleep();
會阻塞線程,使得線程交出時間片,然后處于休眠狀態(tài),直至被重新喚醒;適合用于長時間的等待;
Thread.SpinWait();
使用了自旋等待,等待過程中會進(jìn)行一些的運(yùn)算,線程不會休眠,用于微小的時間等待;長時間等待會影響性能;
Task.Delay();
用于異步中的等待,異步的文章后面才寫,這里先不理會;
這里我們還需要繼續(xù) SpinWait 和 SpinLock 這兩個類型,最后再進(jìn)行總結(jié)對照。
再說自旋和阻塞
前面我們學(xué)習(xí)過自旋和阻塞的區(qū)別,這里再來擼清楚一下。
線程等待有內(nèi)核模式(Kernel Mode)和用戶模式(User Model)。
因?yàn)橹挥胁僮飨到y(tǒng)才能控制線程的生命周期,因此使用 Thread.Sleep()
等方式阻塞線程,發(fā)生上下文切換,此種等待稱為內(nèi)核模式。
用戶模式使線程等待,并不需要線程切換上下文,而是讓線程通過執(zhí)行一些無意義的運(yùn)算,實(shí)現(xiàn)等待。也稱為自旋。
SpinWait 結(jié)構(gòu)
微軟文檔定義:為基于自旋的等待提供支持。
SpinWait 是結(jié)構(gòu)體;Thread.SpinWait() 的原理就是 SpinWait 。
如果你想了解 Thread.SpinWait() 是怎么實(shí)現(xiàn)的,可以參考 https://www.tabsoverspaces.com/233735-how-is-thread-spinwait-actually-implemented
線程阻塞是會耗費(fèi)上下文切換的,對于過短的線程等待,這種切換的代價會比較昂貴的。在我們前面的示例中,大量使用了 Thread.Sleep()
和各種類型的等待方法,這其實(shí)是不合理的。
SpinWait 則提供了更好的選擇。
屬性和方法
老規(guī)矩,先來看一下 SpinWait 常用的屬性和方法。
屬性:
屬性 | 說明 |
---|---|
Count | 獲取已對此實(shí)例調(diào)用 SpinOnce() 的次數(shù)。 |
NextSpinWillYield | 獲取對 SpinOnce() 的下一次調(diào)用是否將產(chǎn)生處理器,同時觸發(fā)強(qiáng)制上下文切換。 |
方法:
方法 | 說明 |
---|---|
Reset() | 重置自旋計(jì)數(shù)器。 |
SpinOnce() | 執(zhí)行單一自旋。 |
SpinOnce(Int32) | 執(zhí)行單一自旋,并在達(dá)到最小旋轉(zhuǎn)計(jì)數(shù)后調(diào)用 Sleep(Int32) 。 |
SpinUntil(Func) | 在指定條件得到滿足之前自旋。 |
SpinUntil(Func, Int32) | 在指定條件得到滿足或指定超時過期之前自旋。 |
SpinUntil(Func, TimeSpan) | 在指定條件得到滿足或指定超時過期之前自旋。 |
自旋示例
下面來實(shí)現(xiàn)一個讓當(dāng)前線程等待其它線程完成任務(wù)的功能。
其功能是開辟一個線程對 sum 進(jìn)行 +1
,當(dāng)新的線程完成運(yùn)算后,主線程才能繼續(xù)運(yùn)行。
class Program { static void Main(string[] args) { new Thread(DoWork).Start(); // 等待上面的線程完成工作 MySleep(); Console.WriteLine("sum = " + sum); Console.ReadKey(); } private static int sum = 0; private static void DoWork() { for (int i = 0; i < 1000_0000; i++) { sum++; } isCompleted = true; } // 自定義等待等待 private static bool isCompleted = false; private static void MySleep() { int i = 0; while (!isCompleted) { i++; } } }
新的實(shí)現(xiàn)
我們改進(jìn)上面的示例,修改 MySleep 方法,改成:
private static bool isCompleted = false; private static void MySleep() { SpinWait wait = new SpinWait(); while (!isCompleted) { wait.SpinOnce(); } }
或者改成
private static bool isCompleted = false; private static void MySleep() { SpinWait.SpinUntil(() => isCompleted); }
SpinLock 結(jié)構(gòu)
微軟文檔:提供一個相互排斥鎖基元,在該基元中,嘗試獲取鎖的線程將在重復(fù)檢查的循環(huán)中等待,直至該鎖變?yōu)榭捎脼橹埂?/p>
SpinLock 稱為自旋鎖,適合用在頻繁爭用而且等待時間較短的場景。主要特征是避免了阻塞,不出現(xiàn)昂貴的上下文切換。
筆者水平有限,關(guān)于 SpinLock ,可以參考 https://www.c-sharpcorner.com/UploadFile/1d42da/spinlock-class-in-threading-C-Sharp/
另外,還記得 Monitor 嘛?SpinLock 跟 Monitor 比較像噢~http://chabaoo.cn/article/237307.htm
在《C#多線程(10:讀寫鎖)》中,我們介紹了 ReaderWriterLock 和 ReaderWriterLockSlim ,而 ReaderWriterLockSlim 內(nèi)部依賴于 SpinLock,并且比 ReaderWriterLock 快了三倍。
屬性和方法
SpinLock 常用屬性和方法如下:
屬性:
屬性 | 說明 |
---|---|
IsHeld | 獲取鎖當(dāng)前是否已由任何線程占用。 |
IsHeldByCurrentThread | 獲取鎖是否已由當(dāng)前線程占用。 |
IsThreadOwnerTrackingEnabled | 獲取是否已為此實(shí)例啟用了線程所有權(quán)跟蹤。 |
方法:
方法 | 說明 |
---|---|
Enter(Boolean) | 采用可靠的方式獲取鎖,這樣,即使在方法調(diào)用中發(fā)生異常的情況下,都能采用可靠的方式檢查 lockTaken 以確定是否已獲取鎖。 |
Exit() | 釋放鎖。 |
Exit(Boolean) | 釋放鎖。 |
TryEnter(Boolean) | 嘗試采用可靠的方式獲取鎖,這樣,即使在方法調(diào)用中發(fā)生異常的情況下,都能采用可靠的方式檢查 lockTaken 以確定是否已獲取鎖。 |
TryEnter(Int32, Boolean) | 嘗試采用可靠的方式獲取鎖,這樣,即使在方法調(diào)用中發(fā)生異常的情況下,都能采用可靠的方式檢查 lockTaken 以確定是否已獲取鎖。 |
TryEnter(TimeSpan, Boolean) | 嘗試采用可靠的方式獲取鎖,這樣,即使在方法調(diào)用中發(fā)生異常的情況下,都能采用可靠的方式檢查 lockTaken 以確定是否已獲取鎖。 |
示例
SpinLock 的模板如下:
private static void DoWork() { SpinLock spinLock = new SpinLock(); bool isGetLock = false; // 是否已獲得了鎖 try { spinLock.Enter(ref isGetLock); // 運(yùn)算 } finally { if (isGetLock) spinLock.Exit(); } }
這里就不寫場景示例了。
需要注意的是, SpinLock 實(shí)例不能共享,也不能重復(fù)使用。
等待性能對比
大佬的文章,.NET 中的多種鎖性能測試數(shù)據(jù):http://kejser.org/synchronisation-in-net-part-3-spinlocks-and-interlocks/
這里我們簡單測試一下阻塞和自旋的性能測試對比。
我們經(jīng)常說,Thread.Sleep()
會發(fā)生上下文切換,出現(xiàn)比較大的性能損失。具體有多大呢?我們來測試一下。(以下運(yùn)算都是在 Debug 下測試)
測試 Thread.Sleep(1)
:
private static void DoWork() { Stopwatch watch = new Stopwatch(); watch.Start(); for (int i = 0; i < 1_0000; i++) { Thread.Sleep(1); } watch.Stop(); Console.WriteLine(watch.ElapsedMilliseconds); }
筆者機(jī)器測試,結(jié)果大約 20018。Thread.Sleep(1)
減去等待的時間 10000 毫秒,那么進(jìn)行 10000 次上下文切換需要花費(fèi) 10000 毫秒,約每次 1 毫秒。
上面示例改成:
for (int i = 0; i < 1_0000; i++) { Thread.Sleep(2); }
運(yùn)算,發(fā)現(xiàn)結(jié)果為 30013,也說明了上下文切換,大約需要一毫秒。
改成 Thread.SpinWait(1000)
:
for (int i = 0; i < 100_0000; i++) { Thread.SpinWait(1000); }
結(jié)果為 28876,說明自旋 1000 次,大約需要 0.03 毫秒。
到此這篇關(guān)于C#多線程系列之線程等待的文章就介紹到這了。希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
C#抓取網(wǎng)頁數(shù)據(jù) 解析標(biāo)題描述圖片等信息 去除HTML標(biāo)簽
本文主要一步一步介紹利用C#抓取頁面數(shù)據(jù)的過程,抓取HTML,獲取標(biāo)題、描述、圖片等信息,并去除HTML,希望對大家有所幫助。2016-04-04C#后臺調(diào)用WebApi接口的實(shí)現(xiàn)方法
本文主要介紹了C#后臺調(diào)用WebApi接口的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06基于C#實(shí)現(xiàn)的HOOK鍵盤鉤子實(shí)例代碼
這篇文章主要介紹了基于C#實(shí)現(xiàn)的HOOK鍵盤鉤子實(shí)例,需要的朋友可以參考下2014-07-07automation服務(wù)器不能創(chuàng)建對象 解決方法
本文主要介紹如何解決“automation服務(wù)器不能創(chuàng)建對象”錯誤,從而解決Visual Studio.Net不能正常使用的問題,需要的朋友可以參考下。2016-06-06C#實(shí)現(xiàn)把圖片轉(zhuǎn)換成二進(jìn)制以及把二進(jìn)制轉(zhuǎn)換成圖片的方法示例
這篇文章主要介紹了C#實(shí)現(xiàn)把圖片轉(zhuǎn)換成二進(jìn)制以及把二進(jìn)制轉(zhuǎn)換成圖片的方法,結(jié)合具體實(shí)例形式分析了基于C#的圖片與二進(jìn)制相互轉(zhuǎn)換以及圖片保存到數(shù)據(jù)庫的相關(guān)操作技巧,需要的朋友可以參考下2017-06-06