并發編程 - 線程同步(八)之自旋鎖SpinLock
前面對互斥鎖Monitor進行了詳細學習,今天我們將繼續學習,一種更輕量級的鎖——自旋鎖SpinLock。

在 C# 中,SpinLock是一個高效的自旋鎖實現,用于提供一種輕量級的鎖機制。SpinLock通過在等待鎖的過程中執行自旋(即不斷嘗試獲取鎖)來避免線程上下文切換,從而減少系統開銷。

SpinLock是一個結構體,使用上和Monitor類很像,都是通過Enter或TryEnter方法持有鎖,同時默認支持lockTaken模式,然后通過Exit釋放鎖。
01、使用示例
下面我們通過啟動10個線程,使用SpinLock鎖分別遞增共享變量_counter,最后再打印出共享變量_counter,代碼如下:
public class SpinLockExample
{
//自旋鎖
private static SpinLock _spinLock = new SpinLock();
//共享資源計數器
private static int _counter = 0;
//計數
public void Count()
{
var lockTaken = false;
try
{
//持有鎖
_spinLock.Enter(ref lockTaken);
//訪問并修改共享資源
_counter++;
var threadId = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine($"線程號:{threadId} 遞增共享變量 _counter 為:{_counter}");
}
finally
{
if (lockTaken)
{
//釋放鎖
_spinLock.Exit();
}
}
}
//打印
public void Print()
{
Console.WriteLine($"---------------------------------------");
Console.WriteLine($"_counter 最終值為:{_counter}");
}
}
public static void SpinLockRun()
{
var example = new SpinLockExample();
//啟動10個線程
var threads = new Thread[10];
for (var i = 0; i < threads.Length; i++)
{
threads[i] = new Thread(example.Count);
threads[i].Start();
}
for (var i = 0; i < threads.Length; i++)
{
threads[i].Join();
}
example.Print();
}
不用看也可以預測出結果為10,執行結果如下:

另外TryEnter方法也和Monitor類同樣支持設置超時時間。
02、小心傳遞SpinLock實例
在傳遞SpinLock實例時,需要十分小心,這是因為SpinLock是結構體即為值類型,當通過值傳遞,會導致創建該結構體的副本,復制一個新的實例,而不是傳遞引用。
如下示例代碼:
public class CopySpinLockExample
{
public void Method1(int thread, SpinLock lockCopy)
{
var lockTaken = false;
//嘗試獲取鎖
lockCopy.Enter(ref lockTaken);
if (lockTaken)
{
Console.WriteLine($"線程 {thread},成功獲取鎖");
}
else
{
Console.WriteLine("線程 {thread},未獲取到鎖");
}
}
}
public static void CopySpinLockRun()
{
var example = new CopySpinLockExample();
SpinLock spinLock = new SpinLock();
example.Method1(1, spinLock);
example.Method1(2, spinLock);
spinLock.Exit();
Console.WriteLine("主線程,釋放鎖");
}
這段代碼有兩個問題是:
1.方法Method1的兩次調用中的lockCopy是各不相同的鎖,即會導致兩次調用都能獲取到鎖;
2.方法Method1中的lockCop和主方法中spinLock是兩個不同的鎖,會導致主方法釋放鎖異常;
我們可以看看代碼執行結果:

方法兩次調用都成功獲取鎖,同時最后釋放鎖時拋出了異常。和我們上面說的兩個問題完全一致。
而要解決這個問題也很簡單,只需要把Method1方法的SpinLock參數前加上ref即可。代碼如下:
public class RefCopySpinLockExample
{
public void Method1(int thread, ref SpinLock lockCopy)
{
var lockTaken = false;
//嘗試獲取鎖
lockCopy.Enter(ref lockTaken);
if (lockTaken)
{
Console.WriteLine($"線程 {thread},成功獲取鎖");
lockCopy.Exit();
Console.WriteLine($"線程 {thread},釋放鎖");
}
else
{
Console.WriteLine("線程 {thread},未獲取到鎖");
}
}
public void Method2(int thread, ref SpinLock lockCopy)
{
var lockTaken = false;
//嘗試獲取鎖
lockCopy.Enter(ref lockTaken);
if (lockTaken)
{
Console.WriteLine($"線程 {thread},成功獲取鎖");
}
else
{
Console.WriteLine("線程 {thread},未獲取到鎖");
}
}
}
public static void RefCopySpinLockRun()
{
var example = new RefCopySpinLockExample();
SpinLock spinLock = new SpinLock();
example.Method1(1, ref spinLock);
example.Method2(2, ref spinLock);
spinLock.Exit();
Console.WriteLine("主線程,釋放鎖");
}
執行結果如下:

從結果上可以發現Method中和主方法中的SpinLock鎖都是同一個了。
03、實現原理
從上面代碼可以發現從使用上來說,SpinLock和互斥鎖Monitor基本一樣,那為什么還要SpinLock呢?
首先互斥鎖Monitor在獲取鎖時會阻塞線程,同時線程會進行上下文切換,把CPU資源讓出來給其他線程使用,直到鎖可用。從這里也可以看出互斥鎖Monitor適用鎖競爭時間較長的場景,否則線程上下文切換比等待資源消耗代價更高就不劃算了。
針對上面提到的問題,就引發了需要一種非阻塞線程的鎖方案,因此SpinLock就應用而生。
如何實現非阻塞線程呢?
首先我們需要理解非阻塞的意義,它是為了解決進行線程上下文切換的代價比鎖的等待代價更大的問題。說白了就是不要讓線程進行上下文切換,比如最簡單粗暴的方式就是直接使用while(true){},使得線程一直處于活動狀態。
而SpinLock底層實現原理的確通過使用while(true){},使得線程原地停留且又不阻塞線程。因為while(true)自動循環的特點才叫自旋鎖。當然SpinLock底層實現不止這么簡單,比如還用到了原子操作Interlocked.CompareExchange。
總結下來SpinLock 的工作原理,大致分為以下兩步:
1.當前線程嘗試獲取鎖,如果獲取成功,進入同步代碼塊。
2.如果未能取鎖(即鎖已經被另一個線程持有),則當前線程會在一個循環(自旋)中重復嘗試,直到獲取到鎖。
SpinLock主要優勢在于它不會將線程掛起即不會發生線程上下文切換,而是讓線程在一個循環(自旋)中等待,直到鎖被釋放后再獲取。同樣因為線程一直自旋等待,如果線程需要等待時間很長又會導致CPU占用過高以及資源浪費。
結合SpinLock實現原理,有如下建議:
1.在需要大量鎖(高并發)并且鎖持有時間又非常短的場景下,特別適合使用SpinLock。
2.避免在單核CPU上使用SpinLock,因為自旋等待會浪費CPU資源。
04、實現一個簡單的自旋鎖
下面我們可以根據SpinLock實現原理來自己實現一個簡單的自旋鎖。
大致思路如下:
1.通過在while(true)循環中,使用原子操作Interlocked.CompareExchange進行設置鎖,從而實現持有鎖方法Enter;
2.通過直接標記鎖狀態為未鎖定狀態,來實現鎖釋放方法Exit;
具體代碼如下:
public class MySpinLock
{
// 0 - 未鎖定, 1 - 鎖定
private volatile int _isLocked = 0;
//獲取鎖
public void Enter()
{
while (true)
{
//使用原子操作檢查和設置鎖
if (Interlocked.CompareExchange(ref _isLocked, 1, 0) == 0)
{
//成功獲得鎖
return;
}
}
}
//釋放鎖
public void Exit()
{
//釋放鎖,直接設置為未鎖定狀態
_isLocked = 0;
}
}
然后把使用示例中的代碼SpinLock替換為MySpinLock即可驗證我們自己的自旋鎖實現,運行結果如下,基本和原生的SpinLock功能一致。

注:測試方法代碼以及示例源碼都已經上傳至代碼庫,有興趣的可以看看。https://gitee.com/hugogoos/Planner

浙公網安備 33010602011771號