C#开发基础之单例模式下的集合数据,解决并发访问读写冲突的问题

1. 前言

在C#中,使用单例模式管理集合数据时,如果多线程同时访问集合,容易产生并发访问的读写冲突问题。单例模式下集合数据的并发访问读写冲突是如何产生的?

单例模式确保一个类在整个应用运行期间只有一个实例,这使得单例对象成为全局共享的资源。当这个单例对象包含集合数据(如List、Dictionary等),并且这些集合数据被多个线程同时访问时,就可能产生并发读写冲突。

2. 并发读写冲突

并发读写冲突通常发生在以下几种情况:

  • 写-写冲突:两个或多个线程同时尝试修改集合中的数据。例如,一个线程正在向集合中添加元素,而另一个线程试图删除元素,或者两个线程同时添加元素,可能导致元素丢失、重复或破坏集合的内部结构。
  • 读-写冲突:一个线程正在读取集合中的数据,而另一个线程同时修改集合(如添加、删除或更新元素)。这可能导致读取线程看到不一致的数据状态,即所谓的"脏读"。
  • 复合操作的不完整性 :当一个操作需要多个步骤完成(如先检查元素是否存在再删除),如果在这过程中被其他线程打断,可能导致不符合预期的结果。
    在单例模式下,如果没有适当的并发控制措施(如锁、同步块、线程安全集合等),这些问题就会变得尤为突出,因为所有对集合的操作都集中在一个共享实例上。

3. 解决策略

为了避免这些问题,可以采取以下几种策略:

  • 使用锁:在访问集合的读写操作前后加锁,确保同一时间只有一个线程能访问集合。这可以是传统的锁机制,如C#中的lock语句关键字。
  • 使用线程安全的集合:许多编程语言提供了专为并发访问设计的线程安全集合,如C#中的ConcurrentDictionary等,这些集合内部实现了必要的同步机制。
  • 不可变集合:使用不可变集合可以在一定程度上避免写冲突,因为一旦创建,集合就不能被改变,只能通过操作返回新的集合实例。但这通常适用于读多写少的场景。
  • 副本和快照:在需要修改集合时,先创建集合的一个副本,然后在副本上操作,最后替换原集合。这种方式减少了锁的范围和时间,但可能增加内存开销。

4. 编码实践

1. 使用lock关键字

可以使用 lock 关键字来同步对集合的访问。lock 能确保在同一时刻只有一个线程能够访问被锁定的代码块,从而避免并发冲突。

css 复制代码
public sealed class Singleton
{
    private static readonly Singleton instance = new Singleton();
    private List<int> data = new List<int>();
    private static readonly object lockObject = new object();

    private Singleton() {}

    public static Singleton Instance
    {
        get { return instance; }
    }

    public void AddData(int value)
    {
        lock (lockObject)
        {
            data.Add(value);
        }
    }

    public List<int> GetData()
    {
        lock (lockObject)
        {
            // 返回副本,防止外部修改原始数据
            return new List<int>(data);
        }
    }
}

2. 使用ConcurrentDictionary或ConcurrentBag

.NET 提供了一些线程安全的集合类,如 ConcurrentDictionary 和 ConcurrentBag。这些集合类内置了线程同步机制,可以直接使用。

css 复制代码
public sealed class Singleton
{
    private static readonly Singleton instance = new Singleton();
    private ConcurrentBag<int> data = new ConcurrentBag<int>();

    private Singleton() {}

    public static Singleton Instance
    {
        get { return instance; }
    }

    public void AddData(int value)
    {
        data.Add(value);
    }

    public List<int> GetData()
    {
        return data.ToList();
    }
}

3. 使用ReaderWriterLockSlim

ReaderWriterLockSlim 允许多个线程同时读取,但写操作是独占的。这种锁非常适合读多写少的场景。

css 复制代码
public sealed class Singleton
{
    private static readonly Singleton instance = new Singleton();
    private List<int> data = new List<int>();
    private static readonly ReaderWriterLockSlim lockSlim = new ReaderWriterLockSlim();

    private Singleton() {}

    public static Singleton Instance
    {
        get { return instance; }
    }

    public void AddData(int value)
    {
        lockSlim.EnterWriteLock();
        try
        {
            data.Add(value);
        }
        finally
        {
            lockSlim.ExitWriteLock();
        }
    }

    public List<int> GetData()
    {
        lockSlim.EnterReadLock();
        try
        {
            return new List<int>(data);
        }
        finally
        {
            lockSlim.ExitReadLock();
        }
    }
}

4. 使用ImmutableCollection

ImmutableCollection 提供了线程安全的集合操作。集合的每次修改都会创建一个新的集合,原有集合保持不变。

css 复制代码
public sealed class Singleton
{
    private static readonly Singleton instance = new Singleton();
    private ImmutableList<int> data = ImmutableList<int>.Empty;
    private static readonly object lockObject = new object();

    private Singleton() {}

    public static Singleton Instance
    {
        get { return instance; }
    }

    public void AddData(int value)
    {
        lock (lockObject)
        {
            data = data.Add(value);
        }
    }

    public ImmutableList<int> GetData()
    {
        // 不需要锁,因为 ImmutableList 是不可变的
        return data;
    }
}

5. 总结

以上几种方法可以有效解决单例模式下集合数据的并发访问问题。具体选择哪种方法取决于你的应用场景和性能需求。如果写操作较少,读操作较多,可以选择 ReaderWriterLockSlim 或 ImmutableCollection。如果需要较高的写性能,可以考虑使用 ConcurrentDictionary 或 ConcurrentBag。

相关推荐
向宇it9 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
九鼎科技-Leo10 小时前
什么是 WPF 中的依赖属性?有什么作用?
windows·c#·.net·wpf
Heaphaestus,RC11 小时前
【Unity3D】获取 GameObject 的完整层级结构
unity·c#
baivfhpwxf202311 小时前
C# 5000 转16进制 字节(激光器串口通讯生成指定格式命令)
开发语言·c#
直裾11 小时前
Scala全文单词统计
开发语言·c#·scala
ZwaterZ13 小时前
vue el-table表格点击某行触发事件&&操作栏点击和row-click冲突问题
前端·vue.js·elementui·c#·vue
ZwaterZ15 小时前
el-table-column自动生成序号&&在序号前插入图标
前端·javascript·c#·vue
SRC_BLUE_1717 小时前
SQLI LABS | Less-55 GET-Challenge-Union-14 Queries Allowed-Variation 2
oracle·c#·less
yngsqq18 小时前
037集——JoinEntities连接多段线polyline和圆弧arc(CAD—C#二次开发入门)
开发语言·c#·swift
Zԅ(¯ㅂ¯ԅ)18 小时前
C#桌面应用制作计算器进阶版01
开发语言·c#