锁定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); } } }
使用此代码,您不会锁定以检查元素的现有元素,但如果未设置元素,则必须在锁定后再次检查。 你有什么收获? 如果元素已存在,则不锁定。