欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

C#加鎖防止并發(fā)的幾種方法詳解

 更新時(shí)間:2025年03月06日 11:29:28   作者:楊文同學(xué)  
文章介紹了C#中多種加鎖方式,包括lock、Monitor、SemaphoreSlim、Mutex、ReaderWriterLockSlim、Concurrent集合、Redis分布式鎖,以及在實(shí)際工作中的應(yīng)用和注意事項(xiàng),感興趣的朋友一起看看吧

前言

在最近的工作中,有一個(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)文章

最新評(píng)論