C# - ConcurrentDictionary 并发场景使用注意事项
1 自身作为 Enumerable 的遍历
自身作为可遍历对象,键值对为元素进行遍历,是线程安全的,但不提供快照,遍历过程中集合产生变更会直接反馈至此次遍历过程中。但并不一定能够保障获取数据的过程中,映射出所有遍历过程中发生的变更,比如,已经遍历过的元素发生了变更或删除,便按已经遍历过的处理了,不会再次处置。
1.1 一起了解源码
1.2 几点概念
- 线程安全:支持并发读写集合,能够保证集合的数据完整性和最终一致性。
- 快照(snapshot):集合快照时的数据内容和状态,原集合发生任何变更都不会体现在快照当中。如发生在遍历等操作前,则操作前集合中是哪些字段就是哪些,后续发生变更也不会反馈到此次操作当中。
- 集合变更:对于集合的增(添加元素)、删(移除元素)、改(变更元素属性)
2 做了 LINQ 操作之后进行遍历
由于没有做快照且没有自身 GetEnumerator 的线程安全处理,对集合做了 Where、OrderBy 等操作后,其对象变为 IEnumerable,此时遍历若集合内元素发生变更,则会报错。
2.1 解决办法
- 使用 ToArray() 或 Values 获取快照
- 再执行对应的 LINQ 操作。则操作针对的是快照,不会因集合发生变更而影响快照内容,从而产生线程安全问题
3 this[index]
- getter:TryGetValue
- setter:TryAddInternal:updateIfExists 为 true
4 TryAdd()
TryAddInternal:updateIfExists 为 false
5 TryUpdate()
- 找得到键,尝试用新值更新旧值。
- 找不到返回 false。
6 私有方法 TryAddInternal()
- 找得到键,返回 false。同时若 updateIfExists 为 true 则用要添加的值更新原值。
- 找不到则尝试添加并返回 true。
6.1 一起了解源码
7 AddOrUpdate()
- TryGetValue,若键存在,TryUpdate。
- 若键不存在,TryAddInternal。
- 如果 try update 或 try add 没有成功返回 false,会反复尝试重新 get 再根据 get 的结果再次 try update 或 try add。
7.1 其所解决的多线程并发问题
- A、B 两个线程同时想向集合添加键相同,值不相同的元素。集合中没有该键。
- A、B 两者都没 get 到 key
- A 先进 add,添加成功,key 对应 A 的值。
- B 后进 add,try add 失败,重新尝试
- 重新尝试 B 的处理,B get 到 key,updateValueFactory 通过将 value 作为引用对象,将 B 所需更新到 value 中的内容,在原有 value 的基础上添加到 value 中,而不影响原有 value(不同于 this[index] 的强制赋值)。
- try update,key 更新 value,同时包含 A 和 B 的内容。
AddOrUpdate 通过重新尝试可以解决并发问题,属于线程安全中不仅保障数据最终一致性,还能够保障数据不丢失的并发处理。
7.2 一起了解源码
8 Values
通过加锁达到快照效果,遍历 Values 的话所遍历的是修改前的集合,遍历过程中集合发生的变更不会体现在此次遍历当中。由于是快照,无论直接遍历还是 Linq 操作后遍历都是线程安全的。
8.1 一起了解源码
9 ToArray()
通过加锁达到快照效果,遍历 ToArray 后的 KeyValuePair 数组的话所遍历的是修改前的集合,遍历过程中集合发生的变更不会体现在此次遍历当中。由于是快照,无论直接遍历还是 Linq 操作后遍历都是线程安全的。