线程安全 具有上限的类似字典的集合
本文关键字:字典 集合 安全 线程 | 更新日期: 2023-09-27 18:33:59
我追求的是具有以下属性的集合:
- 线程安全:它将用于 asp.net,多个客户端可以尝试同时添加、删除和访问成员 最大元素数
- :我希望能够在施工时设置上限,最大元素数
- TryAdd:与
BlockingCollection<T>.TryAdd(T)
相同的方法将是完美的,即如果达到最大元素数,它将返回 false - 类似字典:在大多数其他方面,
ConcurrentDictionary
将是完美的,即能够通过键识别元素,删除任何项目(不仅仅是第一个或最后一个,我认为这将是BlockingCollection
的限制(
在我尝试自己滚动之前,我的问题是:
- 我是否错过了一个内置类型,该类型会为集合中的元素数量设置安全上限?
- 有没有办法以某种方式通过
BlockingCollection
实现此功能?
最后,如果我确实需要尝试自己制作,我应该考虑什么方法? 它像用locks
包裹的Dictionary
一样简单吗?
使用示例:对参与者数量有定义限制的聊天室可以存储参与者的连接信息并拒绝新进入者,直到在 full
最简单的解决方案是创建一个使用普通字典并使用ReaderWriterLockSlim
来控制线程安全访问的包装类。
public class SizeLimitedDictionary<TKey, TValue> : IDictionary<TKey, TValue>
{
private readonly int _maxSize;
private readonly IDictionary<TKey, TValue> _dictionary;
private readonly ReaderWriterLockSlim _readerWriterLock;
public SizeLimitedDictionary(int maxSize)
{
_maxSize = maxSize;
_dictionary = new Dictionary<TKey, TValue>(_maxSize);
_readerWriterLock = new ReaderWriterLockSlim();
}
public bool TryAdd(TKey key, TValue value)
{
_readerWriterLock.EnterWriteLock();
try
{
if (_dictionary.Count >= _maxSize)
return false;
_dictionary.Add(key, value);
}
finally
{
_readerWriterLock.ExitWriteLock();
}
return true;
}
public void Add(TKey key, TValue value)
{
bool added = TryAdd(key, value);
if(!added)
throw new InvalidOperationException("Dictionary is at max size, can not add additional members.");
}
public bool TryAdd(KeyValuePair<TKey, TValue> item)
{
_readerWriterLock.EnterWriteLock();
try
{
if (_dictionary.Count >= _maxSize)
return false;
_dictionary.Add(item);
}
finally
{
_readerWriterLock.ExitWriteLock();
}
return true;
}
public void Add(KeyValuePair<TKey, TValue> item)
{
bool added = TryAdd(item);
if (!added)
throw new InvalidOperationException("Dictionary is at max size, can not add additional members.");
}
public void Clear()
{
_readerWriterLock.EnterWriteLock();
try
{
_dictionary.Clear();
}
finally
{
_readerWriterLock.ExitWriteLock();
}
}
public bool Contains(KeyValuePair<TKey, TValue> item)
{
_readerWriterLock.EnterReadLock();
try
{
return _dictionary.Contains(item);
}
finally
{
_readerWriterLock.ExitReadLock();
}
}
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
{
_readerWriterLock.EnterReadLock();
try
{
_dictionary.CopyTo(array, arrayIndex);
}
finally
{
_readerWriterLock.ExitReadLock();
}
}
public bool Remove(KeyValuePair<TKey, TValue> item)
{
_readerWriterLock.EnterWriteLock();
try
{
return _dictionary.Remove(item);
}
finally
{
_readerWriterLock.ExitWriteLock();
}
}
public int Count
{
get
{
_readerWriterLock.EnterReadLock();
try
{
return _dictionary.Count;
}
finally
{
_readerWriterLock.ExitReadLock();
}
}
}
public bool IsReadOnly
{
get
{
_readerWriterLock.EnterReadLock();
try
{
return _dictionary.IsReadOnly;
}
finally
{
_readerWriterLock.ExitReadLock();
}
}
}
public bool ContainsKey(TKey key)
{
_readerWriterLock.EnterReadLock();
try
{
return _dictionary.ContainsKey(key);
}
finally
{
_readerWriterLock.ExitReadLock();
}
}
public bool Remove(TKey key)
{
_readerWriterLock.EnterWriteLock();
try
{
return _dictionary.Remove(key);
}
finally
{
_readerWriterLock.ExitWriteLock();
}
}
public bool TryGetValue(TKey key, out TValue value)
{
_readerWriterLock.EnterReadLock();
try
{
return _dictionary.TryGetValue(key, out value);
}
finally
{
_readerWriterLock.ExitReadLock();
}
}
public TValue this[TKey key]
{
get
{
_readerWriterLock.EnterReadLock();
try
{
return _dictionary[key];
}
finally
{
_readerWriterLock.ExitReadLock();
}
}
set
{
_readerWriterLock.EnterUpgradeableReadLock();
try
{
var containsKey = _dictionary.ContainsKey(key);
_readerWriterLock.EnterWriteLock();
try
{
if (containsKey)
{
_dictionary[key] = value;
}
else
{
var added = TryAdd(key, value);
if(!added)
throw new InvalidOperationException("Dictionary is at max size, can not add additional members.");
}
}
finally
{
_readerWriterLock.ExitWriteLock();
}
}
finally
{
_readerWriterLock.ExitUpgradeableReadLock();
}
}
}
public ICollection<TKey> Keys
{
get
{
_readerWriterLock.EnterReadLock();
try
{
return _dictionary.Keys;
}
finally
{
_readerWriterLock.ExitReadLock();
}
}
}
public ICollection<TValue> Values
{
get
{
_readerWriterLock.EnterReadLock();
try
{
return _dictionary.Values;
}
finally
{
_readerWriterLock.ExitReadLock();
}
}
}
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{
return _dictionary.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)_dictionary).GetEnumerator();
}
}
此类实现完整的IDictionary<Tkey,TValue>
接口。其工作方式是所有插入都通过TryAdd
,如果您等于或超过最大大小并尝试插入新成员,则从TryAdd
获得false
,从不返回bool
的方法中获得InvalidOperationException
。
我没有使用ConcurrentDictionary
的原因是,在以原子方式添加新成员之前,没有好方法可以尝试检查计数,因此无论如何都需要锁定。您可能会使用并发字典并删除我的所有EnterReadLock
并将EnterWriteLock
替换为正常的lock
调用,但您需要进行性能测试以查看哪个会做得更好。
如果你想要像GetOrAdd
这样的方法,那么自己实现并不难。
无论如何,您最终都会得到自定义实现,也就是说,没有内置类型的行为类似于字典并且具有容量限制......
要使其完全自定义,您可以选择ConcurrentHashSet
限制数量的条目将适合您。
Concurrent HashSet
如果您需要创建具有一些额外功能(例如.max元素(的ConcurrentDictionary
之类的东西,我会选择一种Adaptor
,它将容纳一个私人ConcurrentDictionary
并将其扩展到需要扩展的地方。
许多方法调用将保持不变(您只需调用您的私人ConcurrentDictionary
而不执行任何操作(。
下面是一个简单的实现:
public class ConcurrentDictionaryEx<TKey, TValue>
{
private readonly object _lock = new object();
private ConcurrentDictionary<TKey, TValue> _dic;
public int Capacity { get; set; }
public int Count { get; set; }
public ConcurrentDictionaryEx(int capacity, int concurrencyLevel = 2)
{
this.Capacity = capacity;
_dic = new ConcurrentDictionary<TKey, TValue>(concurrencyLevel, capacity);
}
public bool TryAdd(TKey key, TValue value)
{
lock (_lock)
{
if (this.Count < this.Capacity && _dic.TryAdd(key, value))
{
this.Count++;
return true;
}
return false;
}
}
public bool TryRemove(TKey key, out TValue value)
{
lock (_lock)
{
if (_dic.TryRemove(key, out value))
{
this.Count--;
return true;
}
return false;
}
}
public bool TryGetValue(TKey key, out TValue value)
{
lock (_lock)
{
return _dic.TryGetValue(key, out value);
}
}
public bool TryUpdate(TKey key, TValue newValue, TValue comparisonValue)
{
lock (_lock)
{
return _dic.TryUpdate(key, newValue, comparisonValue);
}
}
}
如果你有所有这些额外的要求,创建一个组成List
而不是一个类不是更好吗?将列表放在您正在创建的类中。
例如,我会说聊天室包含一个列表,而不是一种特殊类型的列表。我会拥有所有最大数量,按名称等逻辑与实际list
分开。然后我会使用一个lock
来与列表的交互,或者一些线程安全的集合,如 ConcurrentBag
.至于您是否想要字典,这实际上取决于数据的细节以及您将如何访问它。
斯科特·张伯伦的回答通过使用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));
}
// A comparer that returns the precalculated HashCode
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
密钥一起使用,请在继续之前查看此 GitHub 问题。
下面是一些涉及四种不同实现的基准测试,字符串值的长度不同。
- 锁定优化:是上面的实现。
- Lock-Simple:是一个简单的
HashSet<T>
+lock
实现,不会预先计算哈希代码。 - ReaderWriterLockSlim:是Scott Chamberlain的实现。
- 本机:是包装
ConcurrentDictionary<T, object>
的实现。它没有界限,所以它不是一个公平的竞争者。此处包含它仅供参考。
所有测试的场景都是相同的:4 个工作线程,从同一哈希集中随机并发读取 (50%( 或添加 (25%( 或删除 (25%( 值。报告的指标是所有工作线程每秒的操作总数。
字符串长度 | 锁定优化 | 锁定-简单 | 读卡器作家锁苗条 | 本机(无界( |
---|---|---|---|---|
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 |