线程安全的类似字典的集合,具有上限。

12

我需要一个具有以下属性的集合:

  • 线程安全:它将在asp.net中使用,多个客户端可以同时尝试添加、删除和访问成员
  • 最大元素数:我希望能够在构造时设置上限,即最大元素数
  • TryAdd:一个与 BlockingCollection<T>.TryAdd(T) 相同的方法将是完美的,即如果达到了最大元素数,它将返回 false
  • 类似于字典:在其他方面,ConcurrentDictionary 将是完美的,即能够通过键标识元素,删除任何项(不仅仅是第一个或最后一个,这可能是 BlockingCollection 的限制)

在我尝试自己实现之前,我的问题是:

  1. 我是否错过了一种内置类型,可以在集合中设置安全上限?
  2. 是否有一种方法可以通过 BlockingCollection 实现此功能?

最后,如果我确实需要尝试制作自己的东西,我应该考虑什么方法?是像包装 Dictionary 一样简单,再加上 locks 吗?

示例用途: 具有参与者数量限制的聊天室可以存储参与者的连接信息,并在满员时拒绝新进入者,直到有空间进入。

6个回答

4

最简单的解决方案就是创建一个包装类,使用普通字典并使用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 这样的方法,自己实现也很容易。

1
ContainsCopyToExitReadLock开始。 我猜这应该是EnterReadLock?不过实现很棒,加1. - default
这绝对是一个比我自己尝试做的更好的版本。非常棒的答案。 - ChrisT
使用ReaderWriterLockSlim相比于声明一些private readonly object myLock = new object();,然后用lock(myLock){...}包围所有关键部分,有什么好处吗?到处都是try ... finally块会影响可读性(在我看来)。 - Corak
没事了,我找到了这个:https://dev59.com/Dm855IYBdhLWcg3wp2N0 - Corak

2
你最终会得到一个定制化的实现,也就是说没有内置类型可以像字典一样并且有容量限制...
为了完全自定义,你可以选择使用 ConcurrentHashSet 来限制条目数量。 .NET Framework 中的 Concurrent HashSet<T>?

1
有趣的是,我认为最终我会选择Nathan答案所暗示的路线,但对于纯粹的问题(不包括示例),我认为这将是正确的方式。 - ChrisT
1
是的,在幕后总会有更多东西,当你完全了解情况时,某些解决方案确实有好处。我怀疑你不会处理数百万个用户,因此 HashSet 的性能优于 List 并不是问题所在。 - Andrew

2
如果您需要创建类似于带有额外功能(例如最大元素)的ConcurrentDictionary,建议使用一个Adaptor,它将持有一个私有的ConcurrentDictionary并在需要扩展时进行扩展。
很多方法调用将保持不变(您只需调用私有的ConcurrentDictionary并什么也不做)。

2
这里是一个简单的实现例子:
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);
        }
    }
}

1

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));
    }

    // 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();
    }
}

这个集合的公共成员包括:
  • 属性: CountIsEmptyBoundedCapacity
  • 方法: ContainsTryGetValueTryAddTryGetOrAddTryRemoveToArray
在内部使用一个不同于 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 开始表现出色。

1
如果你有所有这些额外的要求,那么创建一个组合List的类会比创建一个List更好吗?把列表放在你正在制作的类里面。
例如,我会说聊天室包含一个列表,而不是一种特殊类型的列表。我会把所有最大数量、按名称获取聊天者等逻辑与实际的list分开。然后我会在与列表交互时使用lock,或者像ConcurrentBag这样的线程安全集合。至于是否需要字典,这取决于数据的详细情况以及你将如何访问它。

1
我认为你是对的,也许我对内置功能能够帮助我保护自己过于乐观了。正如你所说,这可能会把责任放错地方。 - ChrisT

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接