ConcurrentDictionary AddOrUpdate抛IndexOutOfRangeException,多线程HashSet操作求解决方案?
解决ConcurrentDictionary中HashSet并发修改导致的IndexOutOfRangeException问题
问题根源
你遇到的System.IndexOutOfRangeException本质是因为**HashSet<T>不是线程安全的集合**。虽然ConcurrentDictionary<TKey,TValue>本身是线程安全的,但它只保证对字典键值对的原子性操作(比如添加、更新、删除键),并不负责保护TValue本身的线程安全。当多个线程同时进入AddOrUpdate的updateValueFactory委托,对同一个HashSet<Job>执行Add或Remove时,HashSet的内部数组结构会被并发修改,从而触发索引越界异常。
有效解决方案
方案1:使用ImmutableHashSet(推荐)
ImmutableHashSet<T>是.NET提供的不可变集合,所有修改操作(Add/Remove)都会返回一个全新的集合实例,完全避免了并发修改同一个集合的风险,天然线程安全。
步骤:
- 首先引用
System.Collections.ImmutableNuGet包(如果项目中没有的话)。 - 修改字典类型和
AddOrUpdate逻辑:
// 初始化线程安全字典,值类型改为ImmutableHashSet<Job> private readonly ConcurrentDictionary<MyKey, ImmutableHashSet<Job>> _dictKeyJob = new ConcurrentDictionary<MyKey, ImmutableHashSet<Job>>(); // 调用AddOrUpdate的逻辑 _dictKeyJob.AddOrUpdate(myKey, key => ImmutableHashSet<Job>.Empty.Add(job), // 首次添加时创建不可变集合 (key, hashJobs) => { if (job.Status == eStatus.Cancelled) { return hashJobs.Remove(job); // 返回删除后的新集合 } else { return hashJobs.Add(job); // 返回添加后的新集合 } });
优势:
- 完全线程安全,无需额外锁机制
- 代码简洁,符合函数式编程风格
- 天然支持事务性修改(要么完全更新,要么不更新)
方案2:自定义线程安全的ConcurrentHashSet
如果需要保留HashSet的原地修改特性,同时保证线程安全,可以基于ConcurrentDictionary<T, byte>实现一个简单的ConcurrentHashSet:
public class ConcurrentHashSet<T> { // 用ConcurrentDictionary存储元素,value用占位字节节省内存 private readonly ConcurrentDictionary<T, byte> _innerDict = new ConcurrentDictionary<T, byte>(); // 添加元素,返回是否成功(元素不存在时添加成功) public bool Add(T item) => _innerDict.TryAdd(item, 0); // 删除元素,返回是否成功(元素存在时删除成功) public bool Remove(T item) => _innerDict.TryRemove(item, out _); // 按需添加其他方法,比如Contains、Count等 public bool Contains(T item) => _innerDict.ContainsKey(item); }
然后修改你的字典和逻辑:
private readonly ConcurrentDictionary<MyKey, ConcurrentHashSet<Job>> _dictKeyJob = new ConcurrentDictionary<MyKey, ConcurrentHashSet<Job>>(); _dictKeyJob.AddOrUpdate(myKey, key => new ConcurrentHashSet<Job> { job }, (key, hashJobs) => { if (job.Status == eStatus.Cancelled) { hashJobs.Remove(job); } else { hashJobs.Add(job); } return hashJobs; });
优势:
- 保留了HashSet的去重特性
- 基于ConcurrentDictionary实现,线程安全有保障
- 支持原地修改集合
方案3:为每个HashSet添加独立锁
如果不想修改集合类型,可以通过为每个key对应的HashSet分配独立锁对象,确保同一时间只有一个线程能修改该集合:
// 额外维护一个存储锁对象的ConcurrentDictionary private readonly ConcurrentDictionary<MyKey, object> _keyLocks = new ConcurrentDictionary<MyKey, object>(); // 操作逻辑 var lockObj = _keyLocks.GetOrAdd(myKey, k => new object()); lock (lockObj) { _dictKeyJob.AddOrUpdate(myKey, key => new HashSet<Job> { job }, (key, hashJobs) => { if (job.Status == eStatus.Cancelled) { hashJobs.Remove(job); } else { hashJobs.Add(job); } return hashJobs; }); }
注意:
- 此方案会额外占用内存存储锁对象,如果key数量极大需要考虑清理不再使用的锁(实现复杂度较高)
- 锁机制会带来一定的性能开销,高并发场景下不如前两种方案高效
内容的提问来源于stack exchange,提问作者rajibdotnet
相关产品推荐
相关产品推荐

