如何在 .Net 中实现 ConcurrentHashSet

Posted

技术标签:

【中文标题】如何在 .Net 中实现 ConcurrentHashSet【英文标题】:How to implement ConcurrentHashSet in .Net 【发布时间】:2011-05-17 10:56:21 【问题描述】:

我正在尝试本着 ConcurrentDictionary 的精神实现一个 ConcurrentHashSet, 采取的方法是使用内部支持 ConcurrentDictionary 并编写小型委托方法,这就是我得到的,但是我坚持使用集合理论方法,尤其是。我不确定我是否可以使用 foreach 并且仍然不会违反并发

public class ConcurrentHashSet<TElement> : ISet<TElement>

    private readonly ConcurrentDictionary<TElement, object> _internal;

    public ConcurrentHashSet(IEnumerable<TElement> elements = null)
    
        _internal = new ConcurrentDictionary<TElement, object>();
        if (elements != null)
            UnionWith(elements);
    

    public void UnionWith(IEnumerable<TElement> other)
    
        if (other == null) throw new ArgumentNullException("other");

        foreach (var otherElement in other)
            Add(otherElement);
    

    public void IntersectWith(IEnumerable<TElement> other)
    
        throw new NotImplementedException();
    

    public void ExceptWith(IEnumerable<TElement> other)
    
        throw new NotImplementedException();
    

    public void SymmetricExceptWith(IEnumerable<TElement> other)
    
        throw new NotImplementedException();
    

    public bool IsSubsetOf(IEnumerable<TElement> other)
    
        throw new NotImplementedException();
    

    public bool IsSupersetOf(IEnumerable<TElement> other)
    
        throw new NotImplementedException();
    

    public bool IsProperSupersetOf(IEnumerable<TElement> other)
    
        throw new NotImplementedException();
    

    public bool IsProperSubsetOf(IEnumerable<TElement> other)
    
        throw new NotImplementedException();
    

    public bool Overlaps(IEnumerable<TElement> other)
    
        return other.Any(otherElement => _internal.ContainsKey(otherElement));
    

    public bool SetEquals(IEnumerable<TElement> other)
    
        int otherCount = 0;
        int thisCount = Count;
        foreach (var otherElement in other)
        
            otherCount++;
            if (!_internal.ContainsKey(otherElement))
                return false;
        
        return otherCount == thisCount;
    

    public bool Add(TElement item)
    
        return _internal.TryAdd(item, null);
    

    public void Clear()
    
        _internal.Clear();
    

    // I am not sure here if that fullfills contract correctly
    void ICollection<TElement>.Add(TElement item)
    
        Add(item);
    

    public bool Contains(TElement item)
    
        return _internal.ContainsKey(item);
    

    public void CopyTo(TElement[] array, int arrayIndex)
    
        _internal.Keys.CopyTo(array, arrayIndex);
    

    public bool Remove(TElement item)
    
        object ignore;
        return _internal.TryRemove(item, out ignore);
    

    public int Count
    
        get  return _internal.Count; 
    

    public bool IsReadOnly
    
        get  return false; 
    

    public IEnumerator<TElement> GetEnumerator()
    
        return _internal.Keys.GetEnumerator();
    

    IEnumerator IEnumerable.GetEnumerator()
    
        return GetEnumerator();
    

【问题讨论】:

尝试购买轻微的并发改进并丢弃 HashSet 的性能是没有意义的。 @Hans,我是否也失去了添加、删除、包含的性能? , 另外我认为我可以在其他线程添加时轻松迭代 Set 【参考方案1】:

我刚刚遇到了一个类似的场景(“我对快速添加、包含和删除感兴趣”)并实现了这个傻瓜:

using System.Collections.Generic;
using System.Threading;

namespace BlahBlah.Utilities

    public class ConcurrentHashSet<T> : IDisposable
    
        private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
        private readonly HashSet<T> _hashSet = new HashSet<T>();

        #region Implementation of ICollection<T> ...ish
        public bool Add(T item)
        
            try
            
                _lock.EnterWriteLock();
                return _hashSet.Add(item);
            
            finally
            
                if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            
        

        public void Clear()
        
            try
            
                _lock.EnterWriteLock();
                _hashSet.Clear();
            
            finally
            
                if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            
        

        public bool Contains(T item)
        
            try
            
                _lock.EnterReadLock();
                return _hashSet.Contains(item);
            
            finally
            
                if (_lock.IsReadLockHeld) _lock.ExitReadLock();
            
        

        public bool Remove(T item)
        
            try
            
                _lock.EnterWriteLock();
                return _hashSet.Remove(item);
            
            finally
            
                if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            
        

        public int Count
        
            get
            
                try
                
                    _lock.EnterReadLock();
                    return _hashSet.Count;
                
                finally
                
                    if (_lock.IsReadLockHeld) _lock.ExitReadLock();
                
            
        
        #endregion

        #region Dispose
        public void Dispose()
        
            if (_lock != null) _lock.Dispose();
        
        #endregion
    

还没有真正测试过它(性能或可靠性方面)。 YMMV。

【讨论】:

请注意,如果您主要进行Contains 呼叫,它的性能会更好。 Add/Clear/Remove 都需要排他锁,所以如果你通过管道传递太多请求,你会失去很多并发性。 你应该释放锁对象。编辑已提交。 @AlirezaNoori,因为 ReaderWriterLockSlim 是托管的,所以终结者 is not needed 在这里 是的,我和你在一起。我通常以这种方式编写 dispose 方法,因为它们对未来的实现更方便。实际上,这里不需要它(这就是您所要求的,相反,我是在告诉您您从发布的链接中已经知道的一些事情)。我以为您问的是一般不需要这 3 个功能。它们不在此处,但如果有人在此处添加非托管资源怎么办?这就是为什么我总是这样写我的 dispose 方法。 @AlirezaNoori 您刚刚使最终确定花费更长的时间,除了复制“Dispose 模式”反模式之外。至少在这种情况下,它并没有除此之外的错误,但唯一没有完成的终结器添加到类中的是错误的新位置。【参考方案2】:

这是一个基于ConcurrentDictionary的并发集的实现:

public class ConcurrentSet<T> : IEnumerable<T>, ISet<T>, ICollection<T>

    private readonly ConcurrentDictionary<T, byte> _dictionary = new ConcurrentDictionary<T, byte>();

    /// <summary>
    /// Returns an enumerator that iterates through the collection.
    /// </summary>
    /// <returns>
    /// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection.
    /// </returns>
    public IEnumerator<T> GetEnumerator()
    
        return _dictionary.Keys.GetEnumerator();
    

    /// <summary>
    /// Returns an enumerator that iterates through a collection.
    /// </summary>
    /// <returns>
    /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
    /// </returns>
    IEnumerator IEnumerable.GetEnumerator()
    
        return GetEnumerator();
    

    /// <summary>
    /// Removes the first occurrence of a specific object from the <see cref="T:System.Collections.Generic.ICollection`1"/>.
    /// </summary>
    /// <returns>
    /// true if <paramref name="item"/> was successfully removed from the <see cref="T:System.Collections.Generic.ICollection`1"/>; otherwise, false. This method also returns false if <paramref name="item"/> is not found in the original <see cref="T:System.Collections.Generic.ICollection`1"/>.
    /// </returns>
    /// <param name="item">The object to remove from the <see cref="T:System.Collections.Generic.ICollection`1"/>.</param><exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only.</exception>
    public bool Remove(T item)
    
        return TryRemove(item);
    

    /// <summary>
    /// Gets the number of elements in the set.
    /// </summary>
    public int Count
    
        get  return _dictionary.Count; 
    

    /// <summary>
    /// Gets a value indicating whether the <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only.
    /// </summary>
    /// <returns>
    /// true if the <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only; otherwise, false.
    /// </returns>
    public bool IsReadOnly  get  return false;  

    /// <summary>
    /// Gets a value that indicates if the set is empty.
    /// </summary>
    public bool IsEmpty
    
        get  return _dictionary.IsEmpty; 
    

    public ICollection<T> Values
    
        get  return _dictionary.Keys; 
    

    /// <summary>
    /// Adds an item to the <see cref="T:System.Collections.Generic.ICollection`1"/>.
    /// </summary>
    /// <param name="item">The object to add to the <see cref="T:System.Collections.Generic.ICollection`1"/>.</param><exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only.</exception>
    void ICollection<T>.Add(T item)
    
        if(!Add(item))
            throw new ArgumentException("Item already exists in set.");
    

    /// <summary>
    /// Modifies the current set so that it contains all elements that are present in both the current set and in the specified collection.
    /// </summary>
    /// <param name="other">The collection to compare to the current set.</param><exception cref="T:System.ArgumentNullException"><paramref name="other"/> is null.</exception>
    public void UnionWith(IEnumerable<T> other)
    
        foreach (var item in other)
            TryAdd(item);
    

    /// <summary>
    /// Modifies the current set so that it contains only elements that are also in a specified collection.
    /// </summary>
    /// <param name="other">The collection to compare to the current set.</param><exception cref="T:System.ArgumentNullException"><paramref name="other"/> is null.</exception>
    public void IntersectWith(IEnumerable<T> other)
    
        var enumerable = other as IList<T> ?? other.ToArray();
        foreach (var item in this)
        
            if (!enumerable.Contains(item))
                TryRemove(item);
        
    

    /// <summary>
    /// Removes all elements in the specified collection from the current set.
    /// </summary>
    /// <param name="other">The collection of items to remove from the set.</param><exception cref="T:System.ArgumentNullException"><paramref name="other"/> is null.</exception>
    public void ExceptWith(IEnumerable<T> other)
    
        foreach (var item in other)
            TryRemove(item);
    

    /// <summary>
    /// Modifies the current set so that it contains only elements that are present either in the current set or in the specified collection, but not both. 
    /// </summary>
    /// <param name="other">The collection to compare to the current set.</param><exception cref="T:System.ArgumentNullException"><paramref name="other"/> is null.</exception>
    public void SymmetricExceptWith(IEnumerable<T> other)
    
        throw new NotImplementedException();
    

    /// <summary>
    /// Determines whether a set is a subset of a specified collection.
    /// </summary>
    /// <returns>
    /// true if the current set is a subset of <paramref name="other"/>; otherwise, false.
    /// </returns>
    /// <param name="other">The collection to compare to the current set.</param><exception cref="T:System.ArgumentNullException"><paramref name="other"/> is null.</exception>
    public bool IsSubsetOf(IEnumerable<T> other)
    
        var enumerable = other as IList<T> ?? other.ToArray();
        return this.AsParallel().All(enumerable.Contains);
    

    /// <summary>
    /// Determines whether the current set is a superset of a specified collection.
    /// </summary>
    /// <returns>
    /// true if the current set is a superset of <paramref name="other"/>; otherwise, false.
    /// </returns>
    /// <param name="other">The collection to compare to the current set.</param><exception cref="T:System.ArgumentNullException"><paramref name="other"/> is null.</exception>
    public bool IsSupersetOf(IEnumerable<T> other)
    
        return other.AsParallel().All(Contains);
    

    /// <summary>
    /// Determines whether the current set is a correct superset of a specified collection.
    /// </summary>
    /// <returns>
    /// true if the <see cref="T:System.Collections.Generic.ISet`1"/> object is a correct superset of <paramref name="other"/>; otherwise, false.
    /// </returns>
    /// <param name="other">The collection to compare to the current set. </param><exception cref="T:System.ArgumentNullException"><paramref name="other"/> is null.</exception>
    public bool IsProperSupersetOf(IEnumerable<T> other)
    
        var enumerable = other as IList<T> ?? other.ToArray();
        return this.Count != enumerable.Count && IsSupersetOf(enumerable);
    

    /// <summary>
    /// Determines whether the current set is a property (strict) subset of a specified collection.
    /// </summary>
    /// <returns>
    /// true if the current set is a correct subset of <paramref name="other"/>; otherwise, false.
    /// </returns>
    /// <param name="other">The collection to compare to the current set.</param><exception cref="T:System.ArgumentNullException"><paramref name="other"/> is null.</exception>
    public bool IsProperSubsetOf(IEnumerable<T> other)
    
        var enumerable = other as IList<T> ?? other.ToArray();
        return Count != enumerable.Count && IsSubsetOf(enumerable);
    

    /// <summary>
    /// Determines whether the current set overlaps with the specified collection.
    /// </summary>
    /// <returns>
    /// true if the current set and <paramref name="other"/> share at least one common element; otherwise, false.
    /// </returns>
    /// <param name="other">The collection to compare to the current set.</param><exception cref="T:System.ArgumentNullException"><paramref name="other"/> is null.</exception>
    public bool Overlaps(IEnumerable<T> other)
    
        return other.AsParallel().Any(Contains);
    

    /// <summary>
    /// Determines whether the current set and the specified collection contain the same elements.
    /// </summary>
    /// <returns>
    /// true if the current set is equal to <paramref name="other"/>; otherwise, false.
    /// </returns>
    /// <param name="other">The collection to compare to the current set.</param><exception cref="T:System.ArgumentNullException"><paramref name="other"/> is null.</exception>
    public bool SetEquals(IEnumerable<T> other)
    
        var enumerable = other as IList<T> ?? other.ToArray();
        return Count == enumerable.Count && enumerable.AsParallel().All(Contains);
    

    /// <summary>
    /// Adds an element to the current set and returns a value to indicate if the element was successfully added. 
    /// </summary>
    /// <returns>
    /// true if the element is added to the set; false if the element is already in the set.
    /// </returns>
    /// <param name="item">The element to add to the set.</param>
    public bool Add(T item)
    
        return TryAdd(item);
    

    public void Clear()
    
        _dictionary.Clear();
    

    public bool Contains(T item)
    
        return _dictionary.ContainsKey(item);
    

    /// <summary>
    /// Copies the elements of the <see cref="T:System.Collections.Generic.ICollection`1"/> to an <see cref="T:System.Array"/>, starting at a particular <see cref="T:System.Array"/> index.
    /// </summary>
    /// <param name="array">The one-dimensional <see cref="T:System.Array"/> that is the destination of the elements copied from <see cref="T:System.Collections.Generic.ICollection`1"/>. The <see cref="T:System.Array"/> must have zero-based indexing.</param><param name="arrayIndex">The zero-based index in <paramref name="array"/> at which copying begins.</param><exception cref="T:System.ArgumentNullException"><paramref name="array"/> is null.</exception><exception cref="T:System.ArgumentOutOfRangeException"><paramref name="arrayIndex"/> is less than 0.</exception><exception cref="T:System.ArgumentException"><paramref name="array"/> is multidimensional.-or-The number of elements in the source <see cref="T:System.Collections.Generic.ICollection`1"/> is greater than the available space from <paramref name="arrayIndex"/> to the end of the destination <paramref name="array"/>.-or-Type <paramref name="T"/> cannot be cast automatically to the type of the destination <paramref name="array"/>.</exception>
    public void CopyTo(T[] array, int arrayIndex)
    
        Values.CopyTo(array, arrayIndex);
    

    public T[] ToArray()
    
        return _dictionary.Keys.ToArray();
    

    public bool TryAdd(T item)
    
        return _dictionary.TryAdd(item, default(byte));
    

    public bool TryRemove(T item)
    
        byte donotcare;
        return _dictionary.TryRemove(item, out donotcare);
    

【讨论】:

在 CopyTo 中,你不应该做 Keys.CopyTo 吗? 非常聪明。竖起大拇指。现在如何处理这些额外的字节;) 集合操作(​​ExceptWith、UnionWith 等)不是线程安全的;另一个线程可能同时在修改集合的内容,所以在 set 操作结束时,结果不会是预期的。 @DavidPfeffer,实际上有;但是您需要使用不可变的内部数据结构,如在 BCL 不可变集合中。但我同意使用可变数据结构(例如 ConcurrentDictionary)是不可行的;我只是在指出事实。 是的。对象具有与之关联的额外信息,用于锁定、将值存储在堆与堆栈中以及其他事情。请参阅此处的答案:***.com/questions/1113819/…【参考方案3】:

ConcurrentDictionary 具有更好的性能特征,因为它使用无锁方式进行读取(至少在 .NET 4.0+ 中)。因此,对于繁重的多线程场景中的性能,ConcurrentDictionary 作为 readerwriterlockslim 包装器的性能可能会更好。但是你需要携带一个空字节作为 dummyvalue(我同意,这看起来很糟糕)。

【讨论】:

我做了一个 ConcurrentSet 的实现,它在幕后正是这样做的。 pastebin.com/8REHRFFL HashSet 有 O(1) 检索。【参考方案4】:

你打算用它做什么?

即使您确实让这些方法起作用,您也可能遇到以下情况:

var set1 = new ConcurrentHashSet<int>();
...

if (set1.Overlaps(set2))

    set1.IntersectWith(set2);
    assert(! set1.IsEmpty());    // might fail

这可能是可以接受的,但 Set 在并发设置中的用处远不如 Queue。

【讨论】:

其实我不一定需要这些功能,我对快速添加和包含和删除(任意)感兴趣,队列没有afaik,我只是不想签入一个类与 NotImplementedException 即使这是一篇很老的帖子。我需要同样的东西,并想分享我解决这个问题的想法。一个好的解决方案是添加一个 TryAddM 方法,类似于并发字典。这样您就可以解决 dataRace 问题

以上是关于如何在 .Net 中实现 ConcurrentHashSet的主要内容,如果未能解决你的问题,请参考以下文章

如何在 .NET 中实现夜间进程?

如何在 ASP.NET 中实现支付网关 [关闭]

如何在 JSON.NET 中实现自定义 JsonConverter?

如何在 vb.net 中实现交易方式?

如何在选择案例语句中实现枚举

如何在 iOS 中实现 Authorize.Net SDK