Scott Chamberlain的答案很好地涵盖了频繁阅读者和不经常写作者的情况,使用了ReaderWriterLockSlim
。但是如果写作与阅读一样频繁呢?在这种情况下,ReaderWriterLockSlim
将不会有太大帮助。
我缓解这种情况的想法是将计算哈希码的操作移出受保护的部分,仅保护涉及共享状态的操作。如果计算值的哈希码的成本不容易,例如当值是较长的字符串时,则应该对此有所裨益。下面是实现此想法构建有限并发HashSet<T>
的示例。该集合使用HashSet<(T, int)>
作为底层存储,其中int
属性是T
值的预计算哈希码:
public class BoundedConcurrentHashSet<T>
{
private readonly HashSet<(T Value, int HashCode)> _set;
private readonly int _boundedCapacity;
private readonly IEqualityComparer<T> _comparer;
public BoundedConcurrentHashSet(int boundedCapacity,
IEqualityComparer<T> comparer = default)
{
_boundedCapacity = boundedCapacity;
_comparer = comparer ?? EqualityComparer<T>.Default;
_set = new(new _Comparer(_comparer));
}
private class _Comparer : IEqualityComparer<(T, int)>
{
private readonly IEqualityComparer<T> _comparer;
public _Comparer(IEqualityComparer<T> comparer) { _comparer = comparer; }
public bool Equals((T, int) x, (T, int) y) => _comparer.Equals(
x.Item1, y.Item1);
public int GetHashCode((T, int) obj) => obj.Item2;
}
public int Count { get { lock (_set) return _set.Count; } }
public bool IsEmpty => Count == 0;
public int BoundedCapacity => _boundedCapacity;
public bool Contains(T value)
{
int hashCode = _comparer.GetHashCode(value);
lock (_set) return _set.Contains((value, hashCode));
}
public bool TryGetValue(T equalValue, out T actualValue)
{
int hashCode = _comparer.GetHashCode(equalValue);
lock (_set)
{
if (_set.TryGetValue((equalValue, hashCode), out var existing))
{
actualValue = existing.Value; return true;
}
actualValue = default; return false;
}
}
public bool TryAdd(T value)
{
int hashCode = _comparer.GetHashCode(value);
lock (_set)
{
if (_set.Count < _boundedCapacity) return _set.Add((value, hashCode));
return false;
}
}
public bool TryGetOrAdd(T equalValue, out T actualValue)
{
int hashCode = _comparer.GetHashCode(equalValue);
lock (_set)
{
if (_set.TryGetValue((equalValue, hashCode), out var existing))
{
actualValue = existing.Value; return true;
}
if (_set.Count < _boundedCapacity)
{
bool added = _set.Add((equalValue, hashCode));
Debug.Assert(added);
actualValue = equalValue; return true;
}
actualValue = default; return false;
}
}
public bool TryRemove(T value)
{
int hashCode = _comparer.GetHashCode(value);
lock (_set) return _set.Remove((value, hashCode));
}
public bool TryRemove(T equalValue, out T actualValue)
{
int hashCode = _comparer.GetHashCode(equalValue);
lock (_set)
{
if (_set.TryGetValue((equalValue, hashCode), out var existing))
{
bool removed = _set.Remove((equalValue, hashCode));
Debug.Assert(removed);
actualValue = existing.Value; return true;
}
actualValue = default; return false;
}
}
public T[] ToArray()
{
lock (_set) return _set.Select(e => e.Value).ToArray();
}
}
这个集合的公共成员包括:
- 属性:
Count
、IsEmpty
和 BoundedCapacity
。
- 方法:
Contains
、TryGetValue
、TryAdd
、TryGetOrAdd
、TryRemove
和 ToArray
。
在内部使用一个不同于 T 的
HashSet<T>
,对于防范
HashDoS 攻击有一定影响。如果您计划将此集合与潜在具有敌意的源的
string
键一起使用,请在继续之前查看
this GitHub 问题。
下面是涉及四种不同实现和字符串值长度不同的一些基准测试结果。
- Lock-Optimized: 是上述实现。
- Lock-Simple: 是一个简单的
HashSet<T>
+lock
实现,它不会预先计算哈希码。
- ReaderWriterLockSlim: 是 Scott Chamberlain 的实现。
- Native: 是一个封装了
ConcurrentDictionary<T, object>
的实现。它没有限制,因此不是公平的竞争者。这里仅作参考。
所有测试的情况都相同:4个工作线程随机并发读取(50%)或添加(25%)或删除(25%)来自同一哈希集的值。报告的指标是所有工作人员每秒执行的总操作数。
字符串长度 |
锁优化 |
简单锁 |
ReaderWriterLockSlim |
本机(无界限) |
10 |
3,833,272 |
4,564,383 |
1,978,146 |
10,369,830 |
30 |
4,196,021 |
4,419,448 |
2,023,593 |
10,697,999 |
100 |
4,024,539 |
3,417,730 |
1,913,043 |
8,365,800 |
300 |
3,952,180 |
2,128,451 |
1,551,082 |
4,644,652 |
1000 |
1,994,425 |
1,018,591 |
839,897 |
2,110,027 |
Lock-Simple 在处理短字符串时表现优于 Lock-Optimized。当字符串长度达到100及以上时,Lock-Optimized 开始表现出色。
Contains
和CopyTo
以ExitReadLock
开始。 我猜这应该是EnterReadLock
?不过实现很棒,加1. - defaultReaderWriterLockSlim
相比于声明一些private readonly object myLock = new object();
,然后用lock(myLock){...}
包围所有关键部分,有什么好处吗?到处都是try ... finally
块会影响可读性(在我看来)。 - Corak