一文帶你了解.Net自旋鎖

本文主要講解.Net基於Thread實現自旋鎖的三種方式
基於Thread.SpinWait實現自旋鎖
實現原理:基於Test–And–Set原子操作實現
使用一個數據表示當前鎖是否已經被獲取 0表示未被索取,1表示已經獲取 獲取鎖時會將_lock的值設置為1 然後檢查修改前的值是否等於0,
優點:
- 不使用Thread.SpinWait方法,重試的方法體會為空,CPU會使用它的最大性能來不斷的進行賦值和比較指令,會浪費很大的性能,Thread.SpinWait提示CPU當前正在自旋鎖的循環中,可以休息若干個時間周期
- 使用自旋鎖需要注意的問題,自旋鎖保護的程式碼應該在非常短的時間內執行完成,如果時間過長,其他執行緒不斷重試導致影響其他執行緒進行
缺點:
- 當前實現沒有考慮到公平性,如果多個執行緒同時獲取鎖失敗,按時間順序第一個獲取鎖的執行緒不一定會在釋放鎖後第一個獲取成功,
程式碼實現:
public static class ThreadSpinWaitDemo
{
private static int _lock = 0;
private static int _counterA = 0;
private static int _counterB = 0;
public static void IncrementCounters()
{
while (Interlocked.Exchange(ref _lock, 1) != 0)
{
Thread.SpinWait(1);
}
++_counterA;
++_counterB;
Interlocked.Exchange(ref _lock, 0);
}
public static void GetCounters(out int counterA, out int counterB)
{
while (Interlocked.Exchange(ref _lock, 1) != 0)
{
Thread.SpinWait(1);
}
counterA = _counterA;
counterB = _counterB;
Interlocked.Exchange(ref _lock, 0);
}
}
基於SpinWaite實現自旋鎖
特性是SpinOnce方法的次數,如果在一定次數以內並且當前邏輯核心所大於1,則調用Thread.SpinWait函數;如果超過一定次數或者當前環境邏輯核心數等於1,則交替使用
Thread.Sleep(0)和Thread.Yield函數,表示切換到其他執行緒,如果再超過一定次數,則讓當前執行緒休眠
SpinWaite解決Thread.SpinWait中的兩個問題
- 如果自旋鎖運行時間超長,SpinWaite可以提示作業系統切換到其他執行緒或者讓當前執行緒進入休眠狀態,
- 如果當前環境只有一個核心邏輯,SpinWaite不會執行Thread.SpinWait函數,而是直接提示作業系統切換到其他執行緒,
public static class ThreadSpinOnceDemo
{
private static int _lock = 0;
private static int _counterA = 0;
private static int _counterB = 0;
public static void IncrementCounters()
{
var spinWait = new SpinWait();
while (Interlocked.Exchange(ref _lock, 1) != 0)
{
spinWait.SpinOnce();
}
++_counterA;
++_counterB;
Interlocked.Exchange(ref _lock, 0);
}
public static void GetCounters(out int counterA, out int counterB)
{
var spinWait = new SpinWait();
while (Interlocked.Exchange(ref _lock, 1) != 0)
{
spinWait.SpinOnce();
}
counterA = _counterA;
counterB = _counterB;
Interlocked.Exchange(ref _lock, 0);
}
}
基於SpinLock實現自旋鎖
封裝了SpinWaite的邏輯
SpinLock程式碼實現
public class ThreadSpinLockDemo
{
private static SpinLock _spinLock = new SpinLock();
private static int _counterA = 0;
private static int _counterB = 0;
public static void IncrementCounters()
{
bool lockTaken = false;
try
{
_spinLock.Enter(ref lockTaken);
++_counterA;
++_counterB;
}
finally
{
if (lockTaken)
{
_spinLock.Exit();
}
}
}
public static void GetCounters(out int counterA, out int counterB)
{
bool lockTaken = false;
try
{
_spinLock.Enter(ref lockTaken);
counterA = _counterA;
counterB = _counterB;
}
finally
{
if (lockTaken)
{
_spinLock.Exit();
}
}
}
}
簡述 Thread.Sleep(0)和Thread.Yield的區別
- 在Windows系統中 Thread.Sleep調用系統提供的SleepEx函數,Thread.Yield函數調用的是系統提供的SwitchToThread方法,
- 區別在於SwitchToThread函數只會切換到當前核心邏輯關聯的待運行隊列的執行緒,不會切換到其他核心邏輯關聯的執行緒上,而SleepEx函數會切換到任意邏輯核心關聯的待運行隊列中的執行緒,並且讓當前執行緒在指定時間內無法重新進入待運行隊列(如果執行緒為0 那麼執行緒可以立刻重新進入待運行隊列)
- 在Linux和OSX中 Thread.Sleep函數在休眠時間不為0時會調用pthread類庫提供的pthread_cond_timedWait函數,在休眠時間為0時會調用sched_yield函數,Thread.Yield同樣會調用sched_yield函數 sched_yield在windows和osx系統中沒有區別,都只會切換到當前和邏輯核心關心的待運行隊列中的執行緒,不會切換到其他核心邏輯關聯的執行緒上。在unix系統上調用系統提供的sleep函數並傳入0 會直接忽略返回
本文基於.Net Core底層入門總結內容
如有哪裡講得不是很明白或是有錯誤,歡迎指正
如您喜歡的話不妨點個贊收藏一下吧🙂
個人微信