锁定HashSet以实现并发

当使用HashSet检查时,是否之前处理了一个项目(即仅使用了Add and Contains )。 此外,当Contains返回false时,它是不相关的,即使它是在之前添加的…

我没有锁定遇到以下exception:

[IndexOutOfRangeException:索引超出了数组的范围。] System.Collections.Generic.HashSet`1.AddIfNotPresent(T value)+6108128

仅锁定Add调用是否足够?

以下似乎永远有效 – 但这不是证据……

 HashSet hashSet = new HashSet(); Parallel.ForEach(GetString(), h => { hashSet.Contains(h); lock(hashSetLock) { hashSet.Add(h); } hashSet.Contains(h); }); 

为了使其精确:我知道在没有锁定的情况下调用Contains是不可线程安全的。 如果上面的代码可能抛出exception或者可能破坏底层数据结构的内部状态(= HashSet),那么我的问题是(接受误报)。

不,仅仅锁定Add是不够的。

它不崩溃的事实只会告诉你它在测试期间没有崩溃。

你无法保证:

  • 它将来不会崩溃
  • 它会产生正确的结果

如果以multithreading方式使用,则非线程安全数据结构无法保证。

你需要:

  • 锁定每次通话
  • 使用线程安全数据结构,该结构是为支持此方案而构建的

如果使用与散列集不同的数据结构(如字典),您甚至可能需要锁定多语句,因为这可能仍然失败:

 lock (dLock) if (d.ContainsKey("test")) return; var value = ExpensiveCallToObtainValue(); lock (dLock) d.Add("test", value); 

在调用ContainsKey和调用Add另一个线程之间可能已经插入了该键。

要在不使用线程安全数据结构的情况下正确处理此问题,请在同一个锁中包含两个操作:

 lock (dLock) { if (!d.ContainsKey("test")) d.Add("test", ExpensiveCallToObtainValue()); } 

不,正如其他人所说,做你正在做的事情并不是线程安全的。 如果底层集合不是线程安全的,则需要锁定每个操作。

使用HashSet ,不需要进行ContainsKey检查,因为Add将检查内部集合是否已包含值 :

返回值类型:System.Boolean

如果将元素添加到HashSet对象,则为true;否则为false。 如果元素已存在,则返回false。

因此,您可以将代码缩小到:

 private readonly object syncRoot = new object(); lock (syncRoot) hashSet.Add(value); 

那些对Contains()调用有什么意义? 他们什么都不做。 如果只想在集合中不包含项目时添加,则可以执行以下操作:

 if(!hasSet.Contains(h)) { lock(hashSetLock) { if(!hasSet.Contains(h)) { hashSet.Add(h); } } } 

使用此代码,您不会锁定以检查元素的现有元素,但如果未设置元素,则必须在锁定后再次检查。 你有什么收获? 如果元素已存在,则不锁定。