Search code examples
c#multithreadingthread-safetylockingmutex

Concurrent HashSet<T> in .NET Framework?


I have the following class.

class Test{
    public HashSet<string> Data = new HashSet<string>();
}

I need to change the field "Data" from different threads, so I would like some opinions on my current thread-safe implementation.

class Test{
    public HashSet<string> Data = new HashSet<string>();

    public void Add(string Val){
            lock(Data) Data.Add(Val);
    }

    public void Remove(string Val){
            lock(Data) Data.Remove(Val);
    }
}

Is there a better solution, to go directly to field and protect it from concurrent access by multiple threads?


Solution

  • Your implementation is correct. The .NET Framework does not provide a built-in concurrent hashset type, unfortunately. However, there are some workarounds.

    ConcurrentDictionary (recommended)

    This first one is to use the class ConcurrentDictionary<TKey, TValue> in the namespace System.Collections.Concurrent. In the case, the value is pointless, so we can use a simple byte (1 byte in memory).

    private ConcurrentDictionary<string, byte> _data;
    

    This is the recommended option because the type is thread-safe and provide you the same advantages than a HashSet<T> except key and value are different objects.

    Source: Social MSDN

    Self-implementation

    Finally, as you did, you can implement your own data type, using lock or other ways that the .NET provides you to be thread-safe. Here is a great example: How to implement ConcurrentHashSet in .Net

    The only drawback of this solution is that the type HashSet<T> doesn't officially concurrent access, even for reading operations.

    I quote the code of the linked post (originally written by Ben Mosher).

    using System;
    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)
            {
                _lock.EnterWriteLock();
                try
                {
                    return _hashSet.Add(item);
                }
                finally
                {
                    if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
                }
            }
    
            public void Clear()
            {
                _lock.EnterWriteLock();
                try
                {
                    _hashSet.Clear();
                }
                finally
                {
                    if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
                }
            }
    
            public bool Contains(T item)
            {
                _lock.EnterReadLock();
                try
                {
                    return _hashSet.Contains(item);
                }
                finally
                {
                    if (_lock.IsReadLockHeld) _lock.ExitReadLock();
                }
            }
    
            public bool Remove(T item)
            {
                _lock.EnterWriteLock();
                try
                {
                    return _hashSet.Remove(item);
                }
                finally
                {
                    if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
                }
            }
    
            public int Count
            {
                get
                {
                    _lock.EnterReadLock();
                    try
                    {
                        return _hashSet.Count;
                    }
                    finally
                    {
                        if (_lock.IsReadLockHeld) _lock.ExitReadLock();
                    }
                }
            }
            #endregion
    
            #region Dispose
            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }
            protected virtual void Dispose(bool disposing)
            {
                if (disposing)
                    if (_lock != null)
                        _lock.Dispose();
            }
            ~ConcurrentHashSet()
            {
                Dispose(false);
            }
            #endregion
        }
    }
    

    EDIT: Move the entrance lock methods ouside the try blocks, as they could throw an exception and execute the instructions contained in the finally blocks.

    ConcurrentBag (inadvisable)

    The usage of ConcurrentBag<T> is not advised, since this type only allows inserting a given element and removing a random element in a thread-safe manner. This class is designed for facilitating producer-consumer scenarios, which is not what OP aims for (more explanations here).

    The other operations (e.g., provided by the extension methods) do not support concurrent usage. MSDN docs warn: "All public and protected members of ConcurrentBag are thread-safe and may be used concurrently from multiple threads. However, members accessed through one of the interfaces the ConcurrentBag implements, including extension methods, are not guaranteed to be thread safe and may need to be synchronized by the caller."