C#加鎖防止并發(fā)的幾種方法詳解
前言
在最近的工作中,有一個(gè)抽獎(jiǎng)的需求。涉及到利益發(fā)放,這時(shí)候就需要加鎖,防止權(quán)益的重復(fù)發(fā)放,避免對(duì)客戶造成經(jīng)濟(jì)損失。在實(shí)際的工作中我用到的是Redis分布式鎖,借此機(jī)會(huì)我學(xué)習(xí)一下C#中各種加鎖的方式,有不對(duì)的地方,歡迎大家指正。
什么時(shí)候需要加鎖
- 多個(gè)線程訪問共享資源
當(dāng)多個(gè)線程訪問共享數(shù)據(jù)(例如共享的列表、字典、文件等)時(shí),可能會(huì)導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)和不一致問題。加鎖可以確保在同一時(shí)刻,只有一個(gè)線程能夠修改共享資源,避免出現(xiàn)并發(fā)問題。
- 并發(fā)導(dǎo)致數(shù)據(jù)不一致問題
多線程并發(fā)執(zhí)行某個(gè)UPDATE語句,可能會(huì)導(dǎo)致數(shù)據(jù)不一致問題。
C#各種加鎖方式
主要介紹lock、Monitor 、SemaphoreSlim、Mutex、ReaderWriterLockSlim、Concurrent、Redis分布式鎖,看完之后其實(shí)不太需要考慮使用哪一種加鎖方式。因?yàn)槊糠N加鎖都有特殊的適用場(chǎng)景。
- lock語句(互斥鎖)
lock語句用于實(shí)現(xiàn)互斥鎖,也是我們比較常見的一種加鎖方式。它是一種同步機(jī)制,用于確保多個(gè)線程在同一時(shí)間只能有一個(gè)線程進(jìn)入特定的代碼塊,其他線程則進(jìn)行等待,直到前一個(gè)線程釋放該鎖。lock細(xì)分的話,也有三種使用方式。
1、lock(this)
lock(this)鎖定的對(duì)象是當(dāng)前實(shí)例(就是該類的實(shí)例),但并不意味著該實(shí)例中的所有方法都會(huì)默認(rèn)加鎖,只有在該實(shí)例中的方法顯式lock加鎖,才能防止多個(gè)線程并發(fā)。
具體使用方法如下:
public class Test
{
public void Get()
{
lock (this) // 鎖定當(dāng)前實(shí)例
{
//執(zhí)行代碼
}
}
}Demo驗(yàn)證
// demo
internal class Program
{
static void Main(string[] args)
{
TestDemo testDemo = new TestDemo();
//開啟新線程,不然單線程執(zhí)行,沒有并發(fā),加鎖也沒有意義
Task.Run(() =>
{
testDemo.Get();
});
Thread.Sleep(2000); //等待2秒,可以讓新線程先加鎖
testDemo.Set();
Console.ReadLine();
}
}
public class TestDemo
{
public void Get(int index)
{
lock (this)
{
Thread.Sleep(5000);
Console.WriteLine("執(zhí)行完成,{index}");
}
}
public void Set()
{
lock (this) //顯式加鎖,不然還是可以并發(fā)執(zhí)行
{
Console.WriteLine("Set完成");
}
}
}執(zhí)行結(jié)果:

如果把Set方法中的鎖去掉,執(zhí)行結(jié)果肯定是反過來的,大家可以自己試一下。
2、lock(privateObj)
lock(privateObj)鎖的是一個(gè)私有的對(duì)象實(shí)例,它基于lock(this)的缺點(diǎn),進(jìn)一步做了改善。privateObj 作為私有對(duì)象,只能在該類的內(nèi)部訪問,外部類無法訪問privateObj,避免了其他地方的代碼可能對(duì)鎖對(duì)象進(jìn)行干擾,減少死鎖的可能。
具體使用方法如下:
public class Test
{
private readonly object _lockObject = new object(); // 私有鎖對(duì)象
public void Get()
{
lock (_lockObject) // 鎖住私有對(duì)象
{
//執(zhí)行代碼
}
}
}Demo驗(yàn)證
internal class Program
{
static void Main(string[] args)
{
TestDemo testDemo = new TestDemo();
Task.Run(() =>
{
testDemo.Get();
});
Thread.Sleep(2000);
testDemo.Set();
Console.ReadLine();
}
}
public class TestDemo
{
private readonly object _lockObject = new object();
public void Get()
{
lock (_lockObject)
{
Thread.Sleep(5000);
Console.WriteLine("Get執(zhí)行完成");
}
}
public void Set()
{
lock (_lockObject)
{
Console.WriteLine("Set執(zhí)行完成");
}
}
}
對(duì)比lock(this)的代碼,其實(shí)沒有太大的改動(dòng)。唯一的區(qū)別就是,lock(this)鎖住的是當(dāng)前類的實(shí)例,而lock(privateObj)鎖住的是該類內(nèi)部一個(gè)私有對(duì)象,鎖的粒度更小一點(diǎn)。????
3、lock(staticObj)
在實(shí)際工作中,上述兩種使用方式(lock(this) 和 lock(privateObj))并不常見。通過上面的代碼,我們可以發(fā)現(xiàn),不論是使用 lock(this) 還是 lock(privateObj),其本質(zhì)上是基于鎖住同一個(gè)對(duì)象實(shí)例來控制并發(fā)。如果并發(fā)時(shí)使用的對(duì)象實(shí)例不同,控制并發(fā)就會(huì)失效。例如,假設(shè)有 10 個(gè)用戶同時(shí)調(diào)用抽獎(jiǎng)接口,每次調(diào)用都會(huì)實(shí)例化一個(gè)新的對(duì)象,這樣就會(huì)導(dǎo)致 10 個(gè)不同的對(duì)象實(shí)例,鎖并不會(huì)生效,仍然會(huì)發(fā)生并發(fā)問題。因此,常見的做法是使用 lock(staticObj),即通過鎖住靜態(tài)對(duì)象來確保多個(gè)請(qǐng)求之間的并發(fā)控制。
lock(staticObj)是鎖定一個(gè)靜態(tài)對(duì)象。由于靜態(tài)對(duì)象在類加載時(shí)只會(huì)被初始化一次,因此它是所有類實(shí)例共享的。
具體使用方法如下:
public class Test
{
private static readonly object _lockObject = new object(); // 靜態(tài)鎖對(duì)象
public void Get()
{
lock (_lockObject) // 鎖住靜態(tài)鎖對(duì)象
{
//執(zhí)行代碼
}
}
}Demo驗(yàn)證
internal class Program
{
static void Main(string[] args)
{
//開啟十個(gè)線程,并行執(zhí)行Get方法
Parallel.For(0, 10, i =>
{
new TestDemo().Get();
});
Console.ReadLine();
}
}
public class TestDemo
{
private static int _count = 0;
private static readonly object _lockObject = new object();
public void Get()
{
lock (_lockObject)
{
_count += 1;
Thread.Sleep(2000); //線程延遲,模擬處理數(shù)據(jù)
Console.WriteLine($@"當(dāng)前值:{_count}");
}
}
}輸出結(jié)果:

如果把鎖去掉,大家可以自行試一下,結(jié)果肯定不是這樣。
lock什么時(shí)候釋放鎖呢,就是lock塊中的代碼執(zhí)行完畢,會(huì)自動(dòng)釋放該鎖。
lock有個(gè)很大一個(gè)缺點(diǎn),lock塊中的代碼,不支持異步操作。
lock本質(zhì)上還是Monitor的語法糖,是在其基礎(chǔ)上包了一層,下面我會(huì)介紹一下Monitor。
對(duì)比
| 特性 | lock(this) | lock(privateObj) | lock(staticObj) |
鎖定對(duì)象 范圍 | 當(dāng)前實(shí)例對(duì)象 | 類內(nèi)部定義的私有對(duì)象 | 類級(jí)別的靜態(tài)對(duì)象 |
| 訪問范圍 | 外部代碼可以訪問實(shí)例對(duì)象并使用鎖,可能導(dǎo)致同步問題 | 僅限類內(nèi)部使用,避免外部訪問 | 所有類實(shí)例共享靜態(tài)對(duì)象,適用于跨實(shí)例同步 |
| 安全性 | 較差,容易導(dǎo)致外部代碼訪問并使用鎖對(duì)象 | 較好,不容易被外部濫用 | 跨實(shí)例同步,適合共享資源的同步 |
| 適用場(chǎng)景 | 單個(gè)實(shí)例的同步,通常不推薦外部訪問鎖對(duì)象 | 推薦用于類內(nèi)部資源的線程同步 | 適用于跨實(shí)例或跨線程的共享資源同步 |
| 死鎖風(fēng)險(xiǎn) | 較高,可能因外部代碼使用this 鎖導(dǎo)致 | 較低,避免了外部訪問鎖對(duì)象的問題 | 較低,但應(yīng)注意跨實(shí)例的鎖順序問題 |
Monitor 類(顯式鎖)
Monitor 也是 C# 中提供的一個(gè)用于線程同步的類,它保證在多線程程序中,只有一個(gè)線程可以同時(shí)訪問某個(gè)共享資源。上面說的lock,其實(shí)是個(gè)語法糖,它在編譯之后其實(shí)生成的代碼就是Monitor。相比較lock,Monitor提供了相對(duì)更多一些的擴(kuò)展功能。
具體使用方法如下:
public class Test
{
//私有靜態(tài)對(duì)象
private static object lockObj = new object();
public void Get()
{
Monitor.Enter(lockObj); //獲取鎖
try
{
//具體執(zhí)行邏輯
}
catch (Exception ex)
{
}
finally
{
Monitor.Exit(lockObj); // 釋放信號(hào)量
}
}
}轉(zhuǎn)到定義,Monitor是一個(gè)靜態(tài)類。

稍微說一下其中幾個(gè)比較重要的方法。
- Enter:進(jìn)入鎖定區(qū)域,阻止其他線程進(jìn)入。
- Exit:退出鎖定區(qū)域,允許其他線程進(jìn)入。
- Wait:會(huì)釋放當(dāng)前線程的鎖,并讓該線程進(jìn)入等待隊(duì)列。當(dāng)前線程會(huì)被掛起,直到其他線程通知它繼續(xù)運(yùn)行(使用 Pulse 或 PulseAll)
- Pulse:會(huì)喚醒等待該對(duì)象鎖的 一個(gè)線程。這個(gè)線程將會(huì)重新獲得鎖并繼續(xù)執(zhí)行。
- PulseAll:會(huì)喚醒 所有 等待該對(duì)象鎖的線程。所有被喚醒的線程會(huì)重新請(qǐng)求該鎖,并繼續(xù)執(zhí)行。
其中有兩個(gè)方法還是挺有意思的,可以先執(zhí)行線程A,然后讓線程A等待,喚醒線程B,線程B執(zhí)行完畢,再喚醒線程A。
基于上面說的,簡(jiǎn)單實(shí)現(xiàn)一個(gè)小Demo
static async Task Main(string[] args)
{
Task.Run(() =>
{
new TestDemo().Get();
});
Task.Run(() =>
{
new TestDemo().Set();
});
Console.ReadLine();
}
public class TestDemo
{
private static object lockObj = new object();
public void Get()
{
Monitor.Enter(lockObj);
try
{
Console.WriteLine($"Get方法開始執(zhí)行");
//釋放當(dāng)前鎖,讓其他等待線程執(zhí)行
Monitor.Wait(lockObj, 1000);
Console.WriteLine($"Get方法執(zhí)行完畢");
}
catch (Exception ex)
{
}
finally
{
// 確保鎖被釋放
Monitor.Exit(lockObj);
}
}
public void Set()
{
Monitor.Enter(lockObj);
try
{
Console.WriteLine($"Set方法開始執(zhí)行");
Thread.Sleep(1000);
Console.WriteLine($"Set方法執(zhí)行完畢");
//喚醒等待的線程
Monitor.Pulse(lockObj);
}
catch (Exception ex)
{
}
finally
{
// 確保鎖被釋放
Monitor.Exit(lockObj);
}
}
}執(zhí)行效果:

SemaphoreSlim(信號(hào)量)
SemaphoreSlim 是 .NET 提供的一種輕量級(jí)同步機(jī)制,用于限制并發(fā)線程數(shù)。它的工作原理類似于操作系統(tǒng)級(jí)的信號(hào)量,但相比于 Semaphore,SemaphoreSlim 更加高效且適合于高性能應(yīng)用程序,因?yàn)樗饕糜趹?yīng)用程序內(nèi)部的線程同步,并且不會(huì)像操作系統(tǒng)信號(hào)量那樣依賴操作系統(tǒng)內(nèi)核,因此在多數(shù)情況下性能更好。
SemaphoreSlim主要的功能是,它可以控制指定多少個(gè)線程同時(shí)訪問共享資源。換句話說,它可以控制并發(fā)執(zhí)行的數(shù)量,并不是某個(gè)方法同一時(shí)刻只能有一個(gè)線程執(zhí)行(SemaphoreSlim 也能支持,將并發(fā)數(shù)設(shè)置為1即可)。
具體使用方法如下:
public class Test
{
//靜態(tài)信號(hào)鎖,同時(shí)可以3個(gè)線程同時(shí)訪問
private static readonly SemaphoreSlim semaphore = new SemaphoreSlim(3);
public void Get()
{
semaphore.Wait(); //獲取鎖
try
{
//具體執(zhí)行邏輯
}
catch (Exception ex)
{
}
finally
{
semaphore.Release(); // 釋放信號(hào)量
}
}
}具體使用方法,大家可以轉(zhuǎn)到定義進(jìn)去看看,其中Wait可以設(shè)置值,如果獲取鎖失敗,可以返回,其他線程就不需要等待了。

下面我用工作中用到的SemaphoreSlim ,作為Demo,場(chǎng)景就是同一時(shí)刻同一個(gè)二維碼只能有一個(gè)人可以參與,防止并發(fā),導(dǎo)致獎(jiǎng)項(xiàng)超量發(fā)出。
public class TestDemo
{
// 使用 SemaphoreSlim + ConcurrentDictionary 來保護(hù)共享資源
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new ConcurrentDictionary<string, SemaphoreSlim>();
//獲取鎖
private static SemaphoreSlim GetLock(string key)
{
return _locks.GetOrAdd(key, new SemaphoreSlim(1, 1));
}
public async Task Get(string key)
{
var semaphore = GetLock(key);
var lockAcquired = false;
try
{
lockAcquired = semaphore.Wait(0); // 嘗試立即獲取信號(hào)量
if (!lockAcquired) return; //獲取鎖失敗,返回,不會(huì)等待上一個(gè)加鎖的線程釋放
//獲取鎖成功,執(zhí)行以下邏輯
}
catch (Exception ex)
{
}
finally
{
if (lockAcquired)
{
// 釋放鎖
semaphore.Release();
// 移除鎖(防止內(nèi)存泄漏)
_locks.TryRemove(key, out _);
}
}
}
}上述這個(gè)例子,是沒用Redis分布式鎖之前,經(jīng)常用的一種加鎖方式。與lock相比,SemaphoreSlim這個(gè)可以使用異步,也可以限制線程并發(fā)的數(shù)量,適用的功能場(chǎng)景也更多。
lock不需要手動(dòng)釋放鎖,執(zhí)行完代碼塊里的內(nèi)容,會(huì)自動(dòng)釋放。但SemaphoreSlim需要調(diào)用Release方法(一定要放到finally中),顯式釋放鎖。
Mutex(互斥體)
Mutex也是一種線程同步機(jī)制,也可以控制多個(gè)線程對(duì)共享資源的訪問。與之前說的lock和SemaphoreSlim有一個(gè)非常大的一個(gè)不同點(diǎn),Mutex是可以跨進(jìn)程使用的,之前說的兩個(gè),只能在一個(gè)進(jìn)程中控制并發(fā)。
具體使用方法如下:
public class Test
{
//創(chuàng)建一個(gè) Mutex鎖
private static Mutex mutex = new Mutex();
public void Get()
{
//獲取 Mutex鎖
mutex.WaitOne();
try
{
//具體執(zhí)行邏輯
}
catch (Exception ex)
{
}
finally
{
//釋放 Mutex鎖
mutex.ReleaseMutex();
}
}
}Mutex如果不需要跨進(jìn)程防止并發(fā),使用方法也很簡(jiǎn)單。無非就是加鎖、獲取鎖、釋放鎖,這里就不再寫代碼演示了,下面主要寫一個(gè)跨進(jìn)程的Demo。
internal class Program
{
static void Main(string[] args)
{
new TestDemo().Get();
Console.WriteLine("全部執(zhí)行完畢");
Console.ReadLine();
}
}
public class TestDemo
{
Mutex mutex = new Mutex(false, "Global\\MutexTest");
public void Get()
{
mutex.WaitOne();
try
{
Thread.Sleep(30000);
Console.WriteLine($"Get執(zhí)行完成");
}
catch (Exception ex)
{
}
finally
{
mutex.ReleaseMutex();
}
}
}Mutex如果需要跨進(jìn)行限制并發(fā),需要加上一個(gè)名稱。命名需要以下注意事項(xiàng):
- 命名時(shí)建議使用 Global\ 或 Local\ 前綴,以明確 Mutex 的作用范圍。
- 在 Windows 系統(tǒng)上,Global\ 適用于所有會(huì)話,Local\ 僅限當(dāng)前會(huì)話。
大家可以再創(chuàng)建一個(gè)控制臺(tái)程序,然后實(shí)例化一個(gè)Mutex,名稱保持一致,啟動(dòng)執(zhí)行,會(huì)發(fā)現(xiàn)不會(huì)立馬執(zhí)行。需要等待另一個(gè)控制臺(tái)程序中的Mutex釋放鎖。這里我就不把我另一個(gè)控制臺(tái)的代碼放出來,很簡(jiǎn)單。
將Mutex轉(zhuǎn)到定義可以看出,Mutex是Threading命名空間下的,并且不支持異步。

ReaderWriterLockSlim(讀寫鎖)
ReaderWriterLockSlim聽名字就知道,該鎖有兩種模式,讀模式和寫模式,它允許多個(gè)線程以讀模式同時(shí)訪問共享資源,但只有一個(gè)線程能夠以寫模式訪問資源。ReaderWriterLockSlim 提供了比傳統(tǒng)的 Monitor 或 lock 更細(xì)粒度的控制,特別適合高并發(fā)讀操作和低頻寫操作的場(chǎng)景。
具體使用方法如下:
public class Test
{
//創(chuàng)建一個(gè) ReaderWriterLockSlim 鎖
private static readonly ReaderWriterLockSlim lockSlim = new ReaderWriterLockSlim();
public void Get()
{
//獲取 讀鎖
lockSlim.EnterReadLock();
//獲取 寫鎖
lockSlim.EnterWriteLock();
try
{
//具體執(zhí)行邏輯
}
catch (Exception ex)
{
}
finally
{
//釋放鎖
lockSlim.ExitReadLock();
lockSlim.ExitWriteLock();
}
}
}- 讀鎖(Read Lock):允許多個(gè)線程并發(fā)地讀取共享資源。只要沒有線程持有寫鎖,多個(gè)線程可以同時(shí)獲取讀鎖。
- 寫鎖(Write Lock):寫鎖是獨(dú)占的,只有一個(gè)線程能夠獲取寫鎖。如果任何線程持有讀鎖或?qū)戞i,其他線程就不能獲得寫鎖。
- 鎖的升級(jí)與降級(jí):ReaderWriterLockSlim 允許線程將鎖從讀鎖升級(jí)到寫鎖(通過 EnterUpgradeableReadLock),但不能將寫鎖降級(jí)為讀鎖。
轉(zhuǎn)到定義看下相關(guān)方法

從源碼中可以看出,獲取鎖可以定時(shí)超時(shí)時(shí)間,還有是否獲取到鎖的屬性、等待讀鎖或者寫鎖的數(shù)量等等,在實(shí)際用到時(shí),大家可以再看看。
下面我根據(jù)可升級(jí)讀鎖,做一個(gè)Demo
internal class Program
{
static void Main(string[] args)
{
Task.Run(() =>
{
new TestDemo().WriteData();
});
Thread.Sleep(1000);
Task.Run(() => {
new TestDemo().ReadData();
});
Console.WriteLine("全部執(zhí)行完畢");
Console.ReadLine();
}
}
public class TestDemo
{
private static readonly ReaderWriterLockSlim lockSlim = new ReaderWriterLockSlim();
public void ReadData()
{
lockSlim.EnterReadLock();
try
{
Console.WriteLine("開始執(zhí)行讀鎖");
Thread.Sleep(1000);
Console.WriteLine("執(zhí)行讀鎖結(jié)束");
}
catch (Exception)
{
}
finally
{
lockSlim.ExitReadLock();
}
}
public void WriteData()
{
//是一個(gè)可升級(jí)的讀鎖
lockSlim.EnterUpgradeableReadLock();
try
{
try
{
Console.WriteLine("開始執(zhí)行寫鎖");
//升級(jí)為寫鎖
lockSlim.EnterWriteLock();
Thread.Sleep(10000);
Console.WriteLine("寫鎖執(zhí)行完畢");
}
catch (Exception)
{
}
finally
{
lockSlim.ExitWriteLock();
}
}
catch (Exception)
{
}
finally
{
lockSlim.EnterUpgradeableReadLock();
}
}
}需要注意的是,ReaderWriterLockSlim中的方法都是成對(duì)出現(xiàn)的,必須在finally中釋放鎖。
Concurrent 集合
Concurrent 集合是為了在多線程環(huán)境下提供線程安全的數(shù)據(jù)結(jié)構(gòu),避免顯式加鎖的復(fù)雜性。System.Collections.Concurrent 命名空間提供了幾種常用的線程安全集合類,如 ConcurrentDictionary、ConcurrentQueue、ConcurrentStack、BlockingCollection 等。
這些集合通過內(nèi)部機(jī)制確保了多線程訪問時(shí)的數(shù)據(jù)一致性,并盡可能避免鎖操作的使用,提升了性能。
- ConcurrentDictionary
ConcurrentDictionary是一個(gè)線程安全的字典類型,用于存儲(chǔ)鍵值對(duì)。它允許多個(gè)線程同時(shí)讀寫,保證在高并發(fā)環(huán)境下的數(shù)據(jù)一致性。與 Dictionary<TKey, TValue> 不同,ConcurrentDictionary 內(nèi)部實(shí)現(xiàn)了更高效的并發(fā)訪問機(jī)制。
- ConcurrentQueue<T>
ConcurrentQueue<T> 是一個(gè)線程安全的隊(duì)列,它采用先進(jìn)先出(FIFO)原則,適用于多個(gè)線程同時(shí)操作隊(duì)列時(shí)使用。支持多個(gè)線程進(jìn)行并發(fā)的入隊(duì)和出隊(duì)操作。
- ConcurrentStack<T>
ConcurrentStack<T> 是一個(gè)線程安全的棧,采用后進(jìn)先出(LIFO)原則。多個(gè)線程可以并發(fā)地執(zhí)行 Push 和 Pop 操作而不需要顯式的鎖。
- BlockingCollection<T>
BlockingCollection<T> 是一個(gè)線程安全的集合類,基于 IProducerConsumerCollection<T> 接口實(shí)現(xiàn)。它允許你在多個(gè)線程之間進(jìn)行生產(chǎn)者-消費(fèi)者模式的操作,并提供阻塞和超時(shí)的機(jī)制。如果集合已滿或?yàn)榭?,調(diào)用 Add 或 Take 方法的線程會(huì)被阻塞,直到集合中有空間或元素可用。
- ConcurrentBag<T>
ConcurrentBag<T> 是一個(gè)線程安全的集合,適用于多個(gè)線程需要無序地向集合中添加和從集合中刪除元素的場(chǎng)景。與其他并發(fā)集合不同,ConcurrentBag<T> 不保證元素的順序,它更適用于那些不關(guān)心順序的場(chǎng)景。
大家需要用到了,可以再詳細(xì)了解一下,我用到了ConcurrentDictionary,這個(gè)比較簡(jiǎn)單,我就不做例子了。
Redis分布式鎖
上面介紹的加鎖方式,都是在同一個(gè)進(jìn)程或多個(gè)進(jìn)程下,還局限于同一個(gè)服務(wù)器。那如果程序是多個(gè)服務(wù)器分布式部署,那么以上的加鎖方式肯定就失效了。解決方案就是用Redis分布式鎖。
Redis 分布式鎖是一種常用的分布式同步機(jī)制,適用于需要多個(gè)服務(wù)協(xié)調(diào)訪問共享資源的場(chǎng)景。Redis 分布式鎖核心是通過 Redis 提供的 原子性操作 來確保多個(gè)客戶端在分布式系統(tǒng)中對(duì)共享資源的互斥訪問。確保在多個(gè)分布式進(jìn)程或節(jié)點(diǎn)之間,每次只有一個(gè)客戶端能夠獲得對(duì)某個(gè)資源的訪問權(quán),防止資源沖突。
需要引用StackExchange.Redis包,下面是一個(gè)簡(jiǎn)易的Demo
static async Task Main(string[] args)
{
Parallel.For(0, 50, async i =>
{
// 連接 Redis
var redis = ConnectionMultiplexer.Connect("localhost");
// 創(chuàng)建鎖管理對(duì)象
var distributedLock = new RedisDistributedLock(redis);
// 鎖標(biāo)識(shí)和唯一值
string lockKey = "CustomerRedisLock";
string lockValue = Guid.NewGuid().ToString();
TimeSpan lockTimeout = TimeSpan.FromSeconds(10);
// 嘗試獲取鎖
bool isLockAcquired = await distributedLock.AcquireLockAsync(lockKey, lockValue, lockTimeout);
if (isLockAcquired)
{
Console.WriteLine("鎖已獲取,執(zhí)行任務(wù)中...");
try
{
// 模擬任務(wù)執(zhí)行
await Task.Delay(2000);
}
finally
{
// 釋放鎖
bool isLockReleased = await distributedLock.ReleaseLockAsync(lockKey, lockValue);
Console.WriteLine(isLockReleased ? "鎖已釋放" : "釋放鎖失敗或鎖已過期");
}
}
else
{
Console.WriteLine("未能獲取鎖");
}
});
Console.ReadLine();
}
public class RedisDistributedLock
{
private readonly IDatabase _redisDatabase;
private readonly TimeSpan _defaultLockTimeout = TimeSpan.FromSeconds(10); // 默認(rèn)鎖超時(shí)時(shí)間
public RedisDistributedLock(IConnectionMultiplexer redisConnection)
{
_redisDatabase = redisConnection.GetDatabase();
}
//獲取鎖
public async Task<bool> AcquireLockAsync(string key, string value, TimeSpan? timeout = null)
{
var lockTimeout = timeout ?? _defaultLockTimeout;
return await _redisDatabase.StringSetAsync(key, value, lockTimeout, When.NotExists);
}
//釋放鎖
public async Task<bool> ReleaseLockAsync(string key, string value)
{
// 獲取當(dāng)前鎖的值
var currentValue = await _redisDatabase.StringGetAsync(key);
// 如果當(dāng)前鎖的值和傳入的值相等,則釋放鎖
if (currentValue == value)
{
return await _redisDatabase.KeyDeleteAsync(key);
}
return false; // 鎖值不匹配,表示鎖已經(jīng)被其他客戶端持有
}
}大家自己可以動(dòng)手封裝一下,這只是簡(jiǎn)易的版本。
總結(jié)
- lock
最簡(jiǎn)單的加鎖方式,是一個(gè)語法糖。缺點(diǎn)就是代碼塊中不支持異步,并且語法比較單一。
Monitor:
短期內(nèi)需要對(duì)共享資源進(jìn)行排他訪問,用于小范圍的同步,性能較好。支持線程等待和喚醒。
缺點(diǎn)是不能控制鎖的粒度(只能針對(duì)方法或代碼塊)
- SemaphoreSlim
可以限制并發(fā)訪問的數(shù)量,缺點(diǎn)是只能在同一進(jìn)程中使用,不能用于跨進(jìn)程同步
- Mutex
用于跨進(jìn)程同步,適合一些需要在不同進(jìn)程中進(jìn)行互斥操作的場(chǎng)景,例如文件操作、共享內(nèi)存等。缺點(diǎn)是由于涉及到操作系統(tǒng)調(diào)用,性能開銷較大。
- ReaderWriterLockSlim
控制鎖的粒度更細(xì),適用讀多寫少的場(chǎng)景。如果你有一個(gè)資源,讀操作比寫操作多,使用 ReaderWriterLockSlim 可以顯著提高并發(fā)性能。
- Concurrent
當(dāng)多個(gè)線程需要并發(fā)地訪問某個(gè)集合時(shí),使用并發(fā)集合可以避免手動(dòng)管理鎖。適用于高并發(fā)的環(huán)境,特別是當(dāng)你需要高效地進(jìn)行元素插入、刪除和查詢時(shí)。
- Redis分布式鎖
支持跨進(jìn)程或跨服務(wù)器的分布式同步,如果你有多個(gè)服務(wù)/服務(wù)器需要同步某些操作,可以使用 Redis 分布式鎖。適用于分布式系統(tǒng)中的任務(wù)調(diào)度、資源共享、任務(wù)獨(dú)占等場(chǎng)景。
Redis 分布式鎖需要注意處理超時(shí)、鎖釋放等問題,通常會(huì)加上超時(shí)機(jī)制避免死鎖。
怎樣加鎖需要考慮具體的場(chǎng)景,也可能兩種加鎖方式一起使用。
加鎖的注意事項(xiàng):
- 加鎖的時(shí)間盡量縮短,所以不必要的代碼,不要放到加鎖的代碼塊中。
- 在控制并發(fā)時(shí),優(yōu)先選擇 Concurrent 集合類。
- 避免鎖的嵌套,多個(gè)鎖嵌套使用時(shí),容易導(dǎo)致死鎖和性能問題。
- 選擇合適的鎖粒度,粗粒度鎖簡(jiǎn)單易用,但性能較差。細(xì)粒度鎖性能高,但編碼復(fù)雜度高。
- 設(shè)置超時(shí)機(jī)制,避免讓線程在加鎖時(shí)長(zhǎng)時(shí)間等待,特別是對(duì)于高并發(fā)場(chǎng)景,可能導(dǎo)致資源浪費(fèi)。
到此這篇關(guān)于C#、加鎖防止并發(fā)的幾種方法的文章就介紹到這了,更多相關(guān)C#加鎖防止并發(fā)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#日期格式強(qiáng)制轉(zhuǎn)換方法(推薦)
下面小編就為大家分享一C#日期格式強(qiáng)制轉(zhuǎn)換的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2017-11-11
C#實(shí)現(xiàn)兩個(gè)richtextbox控件滾動(dòng)條同步滾動(dòng)的簡(jiǎn)單方法
這篇文章主要給大家介紹了C#實(shí)現(xiàn)兩個(gè)richtextbox控件滾動(dòng)條同步滾動(dòng)的簡(jiǎn)單方法,文中介紹的非常詳細(xì),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧。2017-05-05
C#調(diào)用C++DLL傳遞結(jié)構(gòu)體數(shù)組的終極解決方案
這篇文章主要介紹了C#調(diào)用C++DLL傳遞結(jié)構(gòu)體數(shù)組的終極解決方案的相關(guān)資料,需要的朋友可以參考下2017-01-01
快速解決C# android base-64 字符數(shù)組的無效長(zhǎng)度問題
下面小編就為大家?guī)硪黄焖俳鉀QC# android base-64 字符數(shù)組的無效長(zhǎng)度問題。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-08-08

