c# 线程安全和非线程安全的集合有什么区别

线程安全集合与非线程安全集合的本质区别在于多线程同时读写时是否保证数据一致性和不崩溃;ConcurrentDictionary等通过分段锁、原子操作和线程本地存储实现高效并发,而Dictionary等在并发写入时会抛出异常或导致数据错乱。

线程安全集合 vs 非线程安全集合:本质区别在哪

核心区别不是“能不能用”,而是“多线程同时读写时会不会崩”。ConcurrentDictionary 允许多个线程同时 AddTryGetValueRemove,不加锁也不抛 InvalidOperationException;而 Dictionary 在同样场景下大概率崩溃——不是每次必现,但只要发生,就是数据错乱或 Collection was modified 异常。

System.Collections.Concurrent 里的集合怎么做到“不加锁也安全”

它们不用粗粒度的全局锁(比如整个 lock(_dict)),而是靠细粒度控制 + 无锁原子操作:

  • ConcurrentQueueConcurrentStack 完全不用锁,靠 Interlocked.CompareExchange 等 CPU 原子指令完成入队/出栈
  • ConcurrentDictionary 把内部哈希桶分段(默认 31 段),写不同段互不影响;同一段内才用轻量级 SpinLock,避免线程挂起开销
  • ConcurrentBag 为每个线程维护本地队列(ThreadLocalList),添加/取自己线程的数据几乎零竞争;跨线程“偷取”时才做同步

所以它不是“慢但稳”,而是“快且稳”——尤其在高并发、读写混合场景下,比手动用 lock 包裹 List 性能高出数倍。

别误用 Synchronized 包装器:它不是线程安全的“快捷方式”

ArrayList.SynchronizedHashtable.Synchronized 这类老式包装器,只是给每个方法加了同一个对象锁。问题很实在:

  • 所有操作(AddRemoveCount、遍历)都抢同一把锁 → 严重串行化,吞吐量暴跌
  • 看似“线程安全”,但像 if (list.Count > 0) item = list[0]; 这种两步操作,中间可能被其他线程修改 → 仍是竞态条件(race condition)
  • 它属于 System.Collections 非泛型体系,还有装箱/拆箱开销和类型不安全问题

结论:新项目里完全不要用 Synchronized 包装器,直接上 System.Collections.Concurrent 的泛型并发集合。

什么时候该用非线程安全集合

不是“不能用”,而是“不该在共享上下文中用”。适用场景非常明确:

  • 单线程逻辑:比如 MVC Controller 里构造一个 List 做临时计算,返回前就丢弃
  • 只读共享:多个线程只调用 ReadOnlyCollection.AsReadOnly(list)IEnumerable 遍历,且确保源集合创建后不再修改
  • 局部变量 or 方法内集合:生命周期严格绑定当前线程栈,无跨线程传递
  • 性能敏感且已用 lock / ReaderWriterLockSlim 手动保护的临界区(此时用 Dictionary 可能比 ConcurrentDictionary 更省内存)

最容易踩的坑是:以为“我只读不写就安全”,结果忘了某个地方悄悄调了 ToList()ToArray(),返回的是新集合——但原始集合若还在被其他线程写,那这个“只读快照”本身就不一致。

真正要记住的不是“哪个类安全”,而是“谁在访问、怎么访问、生命周期归谁管”。ConcurrentBag 不是万能解药,List 也不是洪水猛兽——关键在上下文。