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è)線程訪問(wèn)共享資源
當(dāng)多個(gè)線程訪問(wèn)共享數(shù)據(jù)(例如共享的列表、字典、文件等)時(shí),可能會(huì)導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)和不一致問(wèn)題。加鎖可以確保在同一時(shí)刻,只有一個(gè)線程能夠修改共享資源,避免出現(xiàn)并發(fā)問(wèn)題。
- 并發(fā)導(dǎo)致數(shù)據(jù)不一致問(wèn)題
多線程并發(fā)執(zhí)行某個(gè)UPDATE語(yǔ)句,可能會(huì)導(dǎo)致數(shù)據(jù)不一致問(wèn)題。
C#各種加鎖方式
主要介紹lock、Monitor 、SemaphoreSlim、Mutex、ReaderWriterLockSlim、Concurrent、Redis分布式鎖,看完之后其實(shí)不太需要考慮使用哪一種加鎖方式。因?yàn)槊糠N加鎖都有特殊的適用場(chǎng)景。
- lock語(yǔ)句(互斥鎖)
lock語(yǔ)句用于實(shí)現(xiàn)互斥鎖,也是我們比較常見(jià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(); //開(kāi)啟新線程,不然單線程執(zhí)行,沒(méi)有并發(fā),加鎖也沒(méi)有意義 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é)果肯定是反過(guò)來(lái)的,大家可以自己試一下。
2、lock(privateObj)
lock(privateObj)鎖的是一個(gè)私有的對(duì)象實(shí)例,它基于lock(this)的缺點(diǎn),進(jìn)一步做了改善。privateObj 作為私有對(duì)象,只能在該類的內(nèi)部訪問(wèn),外部類無(wú)法訪問(wèn)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í)沒(méi)有太大的改動(dòng)。唯一的區(qū)別就是,lock(this)鎖住的是當(dāng)前類的實(shí)例,而lock(privateObj)鎖住的是該類內(nèi)部一個(gè)私有對(duì)象,鎖的粒度更小一點(diǎn)。????
3、lock(staticObj)
在實(shí)際工作中,上述兩種使用方式(lock(this) 和 lock(privateObj))并不常見(jiàn)。通過(guò)上面的代碼,我們可以發(fā)現(xiàn),不論是使用 lock(this) 還是 lock(privateObj),其本質(zhì)上是基于鎖住同一個(gè)對(duì)象實(shí)例來(lái)控制并發(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ā)問(wèn)題。因此,常見(jiàn)的做法是使用 lock(staticObj),即通過(guò)鎖住靜態(tài)對(duì)象來(lái)確保多個(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) { //開(kāi)啟十個(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的語(yǔ)法糖,是在其基礎(chǔ)上包了一層,下面我會(huì)介紹一下Monitor。
對(duì)比
特性 | lock(this) | lock(privateObj) | lock(staticObj) |
鎖定對(duì)象 范圍 | 當(dāng)前實(shí)例對(duì)象 | 類內(nèi)部定義的私有對(duì)象 | 類級(jí)別的靜態(tài)對(duì)象 |
訪問(wèn)范圍 | 外部代碼可以訪問(wèn)實(shí)例對(duì)象并使用鎖,可能導(dǎo)致同步問(wèn)題 | 僅限類內(nèi)部使用,避免外部訪問(wèn) | 所有類實(shí)例共享靜態(tài)對(duì)象,適用于跨實(shí)例同步 |
安全性 | 較差,容易導(dǎo)致外部代碼訪問(wèn)并使用鎖對(duì)象 | 較好,不容易被外部濫用 | 跨實(shí)例同步,適合共享資源的同步 |
適用場(chǎng)景 | 單個(gè)實(shí)例的同步,通常不推薦外部訪問(wèn)鎖對(duì)象 | 推薦用于類內(nèi)部資源的線程同步 | 適用于跨實(shí)例或跨線程的共享資源同步 |
死鎖風(fēng)險(xiǎn) | 較高,可能因外部代碼使用this 鎖導(dǎo)致 | 較低,避免了外部訪問(wèn)鎖對(duì)象的問(wèn)題 | 較低,但應(yīng)注意跨實(shí)例的鎖順序問(wèn)題 |
Monitor 類(顯式鎖)
Monitor 也是 C# 中提供的一個(gè)用于線程同步的類,它保證在多線程程序中,只有一個(gè)線程可以同時(shí)訪問(wèn)某個(gè)共享資源。上面說(shuō)的lock,其實(shí)是個(gè)語(yǔ)法糖,它在編譯之后其實(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)類。
稍微說(shuō)一下其中幾個(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。
基于上面說(shuō)的,簡(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方法開(kāi)始執(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方法開(kāi)始執(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í)訪問(wèn)共享資源。換句話說(shuō),它可以控制并發(fā)執(zhí)行的數(shù)量,并不是某個(gè)方法同一時(shí)刻只能有一個(gè)線程執(zhí)行(SemaphoreSlim 也能支持,將并發(fā)數(shù)設(shè)置為1即可)。
具體使用方法如下:
public class Test { //靜態(tài)信號(hào)鎖,同時(shí)可以3個(gè)線程同時(shí)訪問(wèn) 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 來(lái)保護(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è)例子,是沒(méi)用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ì)共享資源的訪問(wèn)。與之前說(shuō)的lock和SemaphoreSlim有一個(gè)非常大的一個(gè)不同點(diǎn),Mutex是可以跨進(jìn)程使用的,之前說(shuō)的兩個(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)單。無(wú)非就是加鎖、獲取鎖、釋放鎖,這里就不再寫代碼演示了,下面主要寫一個(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)的代碼放出來(lái),很簡(jiǎn)單。
將Mutex轉(zhuǎn)到定義可以看出,Mutex是Threading命名空間下的,并且不支持異步。
ReaderWriterLockSlim(讀寫鎖)
ReaderWriterLockSlim聽(tīng)名字就知道,該鎖有兩種模式,讀模式和寫模式,它允許多個(gè)線程以讀模式同時(shí)訪問(wèn)共享資源,但只有一個(gè)線程能夠以寫模式訪問(wèn)資源。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ā)地讀取共享資源。只要沒(méi)有線程持有寫鎖,多個(gè)線程可以同時(shí)獲取讀鎖。
- 寫鎖(Write Lock):寫鎖是獨(dú)占的,只有一個(gè)線程能夠獲取寫鎖。如果任何線程持有讀鎖或?qū)戞i,其他線程就不能獲得寫鎖。
- 鎖的升級(jí)與降級(jí):ReaderWriterLockSlim 允許線程將鎖從讀鎖升級(jí)到寫鎖(通過(guò) 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("開(kāi)始執(zhí)行讀鎖"); Thread.Sleep(1000); Console.WriteLine("執(zhí)行讀鎖結(jié)束"); } catch (Exception) { } finally { lockSlim.ExitReadLock(); } } public void WriteData() { //是一個(gè)可升級(jí)的讀鎖 lockSlim.EnterUpgradeableReadLock(); try { try { Console.WriteLine("開(kāi)始執(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 等。
這些集合通過(guò)內(nèi)部機(jī)制確保了多線程訪問(wèn)時(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ā)訪問(wèn)機(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è)線程需要無(wú)序地向集合中添加和從集合中刪除元素的場(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)訪問(wèn)共享資源的場(chǎng)景。Redis 分布式鎖核心是通過(guò) Redis 提供的 原子性操作 來(lái)確保多個(gè)客戶端在分布式系統(tǒng)中對(duì)共享資源的互斥訪問(wèn)。確保在多個(gè)分布式進(jìn)程或節(jié)點(diǎn)之間,每次只有一個(gè)客戶端能夠獲得對(duì)某個(gè)資源的訪問(wèn)權(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 ? "鎖已釋放" : "釋放鎖失敗或鎖已過(guò)期"); } } 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è)語(yǔ)法糖。缺點(diǎn)就是代碼塊中不支持異步,并且語(yǔ)法比較單一。
Monitor:
短期內(nèi)需要對(duì)共享資源進(jìn)行排他訪問(wèn),用于小范圍的同步,性能較好。支持線程等待和喚醒。
缺點(diǎn)是不能控制鎖的粒度(只能針對(duì)方法或代碼塊)
- SemaphoreSlim
可以限制并發(fā)訪問(wèn)的數(shù)量,缺點(diǎn)是只能在同一進(jìn)程中使用,不能用于跨進(jìn)程同步
- Mutex
用于跨進(jìn)程同步,適合一些需要在不同進(jìn)程中進(jìn)行互斥操作的場(chǎng)景,例如文件操作、共享內(nèi)存等。缺點(diǎn)是由于涉及到操作系統(tǒng)調(diào)用,性能開(kāi)銷較大。
- ReaderWriterLockSlim
控制鎖的粒度更細(xì),適用讀多寫少的場(chǎng)景。如果你有一個(gè)資源,讀操作比寫操作多,使用 ReaderWriterLockSlim 可以顯著提高并發(fā)性能。
- Concurrent
當(dāng)多個(gè)線程需要并發(fā)地訪問(wèn)某個(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í)、鎖釋放等問(wèn)題,通常會(huì)加上超時(shí)機(jī)制避免死鎖。
怎樣加鎖需要考慮具體的場(chǎng)景,也可能兩種加鎖方式一起使用。
加鎖的注意事項(xiàng):
- 加鎖的時(shí)間盡量縮短,所以不必要的代碼,不要放到加鎖的代碼塊中。
- 在控制并發(fā)時(shí),優(yōu)先選擇 Concurrent 集合類。
- 避免鎖的嵌套,多個(gè)鎖嵌套使用時(shí),容易導(dǎo)致死鎖和性能問(wèn)題。
- 選擇合適的鎖粒度,粗粒度鎖簡(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ì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2017-11-11C#實(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à)值,需要的朋友們下面來(lái)一起看看吧。2017-05-05C#調(diào)用C++DLL傳遞結(jié)構(gòu)體數(shù)組的終極解決方案
這篇文章主要介紹了C#調(diào)用C++DLL傳遞結(jié)構(gòu)體數(shù)組的終極解決方案的相關(guān)資料,需要的朋友可以參考下2017-01-01C#創(chuàng)建一個(gè)Word并打開(kāi)的方法
這篇文章主要介紹了C#創(chuàng)建一個(gè)Word并打開(kāi)的方法,實(shí)例分析了C#操作word的常用技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-04-04快速解決C# android base-64 字符數(shù)組的無(wú)效長(zhǎng)度問(wèn)題
下面小編就為大家?guī)?lái)一篇快速解決C# android base-64 字符數(shù)組的無(wú)效長(zhǎng)度問(wèn)題。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-08-08