C# - ConcurrentDictionary 并发场景使用注意事项
阅读原文时间:2023年08月27日阅读:1

自身作为可遍历对象,键值对为元素进行遍历,是线程安全的,但不提供快照,遍历过程中集合产生变更会直接反馈至此次遍历过程中。但并不一定能够保障获取数据的过程中,映射出所有遍历过程中发生的变更,比如,已经遍历过的元素发生了变更或删除,便按已经遍历过的处理了,不会再次处置。

1.1 一起了解源码

GetEnumerator()

1.2 几点概念

  1. 线程安全:支持并发读写集合,能够保证集合的数据完整性和最终一致性。
  2. 快照(snapshot):集合快照时的数据内容和状态,原集合发生任何变更都不会体现在快照当中。如发生在遍历等操作前,则操作前集合中是哪些字段就是哪些,后续发生变更也不会反馈到此次操作当中。
  3. 集合变更:对于集合的增(添加元素)、删(移除元素)、改(变更元素属性)

由于没有做快照且没有自身 GetEnumerator 的线程安全处理,对集合做了 Where、OrderBy 等操作后,其对象变为 IEnumerable,此时遍历若集合内元素发生变更,则会报错。

2.1 解决办法

  1. 使用 ToArray() 或 Values 获取快照
  2. 再执行对应的 LINQ 操作。则操作针对的是快照,不会因集合发生变更而影响快照内容,从而产生线程安全问题
  • getter:TryGetValue
  • setter:TryAddInternal:updateIfExists 为 true

TryAddInternal:updateIfExists 为 false

  • 找得到键,尝试用新值更新旧值。

  • 找不到返回 false。

  • 找得到键,返回 false。同时若 updateIfExists 为 true 则用要添加的值更新原值。

  • 找不到则尝试添加并返回 true。

6.1 一起了解源码

private bool TryAddInternal(TKey key, TValue value, bool updateIfExists, bool acquireLock, out TValue resultingValue)

  1. TryGetValue,若键存在,TryUpdate。
  2. 若键不存在,TryAddInternal。
  3. 如果 try update 或 try add 没有成功返回 false,会反复尝试重新 get 再根据 get 的结果再次 try update 或 try add。

7.1 其所解决的多线程并发问题

  1. A、B 两个线程同时想向集合添加键相同,值不相同的元素。集合中没有该键。
  2. A、B 两者都没 get 到 key
  3. A 先进 add,添加成功,key 对应 A 的值。
  4. B 后进 add,try add 失败,重新尝试
  5. 重新尝试 B 的处理,B get 到 key,updateValueFactory 通过将 value 作为引用对象,将 B 所需更新到 value 中的内容,在原有 value 的基础上添加到 value 中,而不影响原有 value(不同于 this[index] 的强制赋值)。
  6. try update,key 更新 value,同时包含 A 和 B 的内容。

AddOrUpdate 通过重新尝试可以解决并发问题,属于线程安全中不仅保障数据最终一致性,还能够保障数据不丢失的并发处理。

7.2 一起了解源码

AddOrUpdate()

通过加锁达到快照效果,遍历 Values 的话所遍历的是修改前的集合,遍历过程中集合发生的变更不会体现在此次遍历当中。由于是快照,无论直接遍历还是 Linq 操作后遍历都是线程安全的。

8.1 一起了解源码

GetValues()

通过加锁达到快照效果,遍历 ToArray 后的 KeyValuePair 数组的话所遍历的是修改前的集合,遍历过程中集合发生的变更不会体现在此次遍历当中。由于是快照,无论直接遍历还是 Linq 操作后遍历都是线程安全的。

9.1 一起了解源码

ToArray()