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?
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
ConcurrentBag
If you don't mind about the duplicate entries, you can use the class ConcurrentBag<T>
in the same namespace of the previous class.
private ConcurrentBag<string> _data;
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.