.NET Framework中并发HashSet <T>?


151

我有以下课程。

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

我需要从不同的线程更改字段“数据”,所以我想对当前的线程安全实现提出一些意见。

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

有没有更好的解决方案,可以直接进入现场并保护它免受多个线程的并发访问?


如何使用System.Collections.Concurrent
I4V

8
当然,将其设为私有。
汉斯·帕桑

3
从并发的角度来看,除了“数据”字段公开之外,您所做的事情没有什么错!如果担心的话,使用ReaderWriterLockSlim可以获得更好的读取性能。 msdn.microsoft.com/en-us/library/…–
艾伦·埃尔德

ReaderWriterLock当多个读者和一个作者时,@ AllanElder 会有所帮助(高效)。我们必须知道OP是否如此
Sriram Sakthivel

2
当前的实现不是真正的“并发” :)只是线程安全的。
2013年

Answers:


164

您的实现是正确的。不幸的是,.NET Framework没有提供内置的并发哈希集类型。但是,有一些解决方法。

ConcurrentDictionary(推荐)

第一个是使用ConcurrentDictionary<TKey, TValue>命名空间中的类System.Collections.Concurrent。在这种情况下,该值是没有意义的,因此我们可以使用一个简单的byte(内存中为1个字节)。

private ConcurrentDictionary<string, byte> _data;

推荐使用此选项,因为该类型是线程安全的,并且HashSet<T>除了键和值是不同的对象之外,还为您提供了相同的优点。

来源:社交MSDN

并发袋

如果您不介意重复的条目,则可以ConcurrentBag<T>在上一个类的相同命名空间中使用该类。

private ConcurrentBag<string> _data;

自我实现

最后,像您所做的那样,您可以使用锁或.NET为您提供线程安全的其他方式来实现自己的数据类型。这是一个很好的例子:如何在.Net中实现ConcurrentHashSet

该解决方案的唯一缺点是HashSet<T>,即使对于读取操作,该类型也无法进行正式的并发访问。

我引用链接文章的代码(最初由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
    }
}

编辑:移动入口锁定方法将try块移开,因为它们可能引发异常并执行finally块中包含的指令。


8
具有垃圾值的字典是一个列表
Ralf

44
@Ralf好吧,这是一个集合,而不是一个列表,因为它是无序的。
Servy 2013年

11
根据MSDN关于“集合和同步(线程安全)”的简短文档,System.Collections和相关命名空间中的类可以被多个线程安全地读取。这意味着HashSet可以被多个线程安全地读取。
汉克·舒尔茨

7
@Oliver,即使null引用是引用,引用也要为每个条目使用更多的内存(引用在32位运行时中需要4个字节,在64位运行时中需要8个字节)。因此,使用byte,空结构或类似结构可以减少内存占用(或者,如果运行时在本地内存边界上对齐数据以加快访问速度,则可能不会减少内存占用)。
Lucero 2014年

4
自我实现不是ConcurrentHashSet,而是ThreadSafeHashSet。这两个之间有很大的不同,这就是为什么Micorosft放弃SynchronizedCollections(人们弄错了)的原因。为了成为“并发”操作,应实现GetOrAdd等操作(如字典),否则,如果没有其他锁定,就无法确保并发性。但是,如果您需要在类外进行其他锁定,那为什么不从一开始就使用简单的HashSet?
乔治·马夫里斯基斯基

36

我创建了一个基于的实际值,而不是包装ConcurrentDictionary或锁定了。HashSetConcurrentHashSetConcurrentDictionary

此实现支持没有HashSet设置操作的每个项目的基本操作,因为它们在IMO并发场景中意义不大:

var concurrentHashSet = new ConcurrentHashSet<string>(
    new[]
    {
        "hamster",
        "HAMster",
        "bar",
    },
    StringComparer.OrdinalIgnoreCase);

concurrentHashSet.TryRemove("foo");

if (concurrentHashSet.Contains("BAR"))
{
    Console.WriteLine(concurrentHashSet.Count);
}

输出2

您可以从的NuGet得到它在这里和在Github上查看源在这里


3
这应该是公认的答案,很好的实现
smirkingman

不应该将Add重命名为TryAdd以便与ConcurrentDictionary一致吗?

8
@Neo否...,因为它有意使用HashSet <T>语义,您在其中调用Add,它返回一个布尔值,指示该项是已添加(true)还是已存在(false)。 msdn.microsoft.com/en-us/library/bb353005(v=vs.110).aspx
G-Mac

它不应该实现ISet<T>接口bo实际匹配HashSet<T>语义吗?
Nekromancer

1
正如我在回答中所说的,@ Nekromancer,我认为在并发实现中提供这些set方法没有任何意义。 Overlaps例如,要么需要在整个实例运行期间锁定实例,要么提供可能已经错误的答案。这两种选择都是不好的IMO(可以由用户在外部添加)。
i3arnon

21

既然没有人提到过,我将提供一种可能适合或不适合您特定目的的替代方法:

Microsoft不可变集合

来自MS团队背后的博客文章

尽管并发创建和运行比以往任何时候都容易,但基本问题之一仍然存在:可变的共享状态。从多个线程读取通常很容易,但是一旦需要更新状态,状态就会变得困难得多,尤其是在需要锁定的设计中。

锁定的替代方法是使用不可变状态。保证不变的数据结构永远不会改变,因此可以在不同线程之间自由传递,而不必担心踩到别人的脚趾。

但是,这种设计带来了一个新问题:如何在不每次都复制整个状态的情况下管理状态更改?当涉及集合时,这尤其棘手。

这是不可变集合的来源。

这些集合包括ImmutableHashSet <T>ImmutableList <T>

性能

由于不可变集合使用下面的树数据结构来实现结构共享,因此它们的性能特征不同于可变集合。与锁定可变集合进行比较时,结果将取决于锁定争用和访问模式。但是,取自关于不可变集合的另一篇博客文章

问:我听说不可变的收藏很慢。这些有什么不同吗?当性能或内存很重要时,可以使用它们吗?

答:这些不可变的集合已经过高度调整,在平衡内存共享的同时具有与可变集合竞争的性能特征。在某些情况下,它们在算法上和实际时间上几乎与可变集合一样快,有时甚至更快,而在其他情况下,它们在算法上更复杂。但是,在许多情况下,差异可以忽略不计。通常,您应该使用最简单的代码来完成工作,然后根据需要调整性能。不可变的集合可帮助您编写简单的代码,尤其是在必须考虑线程安全性的情况下。

换句话说,在许多情况下,差异不会很明显,您应该选择更简单的选择-对于并发集,将使用ImmutableHashSet<T>,因为您没有现有的锁定可变实现!:-)


1
ImmutableHashSet<T>如果您的意图是从多个线程更新共享状态,还是没有什么帮助呢?
tugberk

7
@tugberk是的,不是。由于集合是不可变的,因此您将不得不更新对此集合的引用,而集合本身并不能帮助您。好消息是,您已将更新共享数据结构的复杂问题从多个线程减少到了更新共享引用的简单得多的问题。该库为您提供了ImmutableInterlocked.Update方法来帮助您。
索伦Boisen

1
@SørenBoisenjust阅读了有关不可变集合的信息,并试图弄清如何使用它们是线程安全的。ImmutableInterlocked.Update似乎是缺少的链接。谢谢!
xneg

4

进行ISet<T>并发的棘手部分是设置方法(联合,交集,差)本质上是迭代的。至少必须在操作涉及的两个集合中的所有n个成员上进行迭代。

ConcurrentDictionary<T,byte>当您必须在迭代过程中锁定整个集合时,就会失去a的优势。如果没有锁定,则这些操作不是线程安全的。

考虑到的额外开销ConcurrentDictionary<T,byte>,使用更轻的重量HashSet<T>并把所有东西都锁在锁中可能是更明智的选择。

如果不需要设置操作,请在添加键时使用,ConcurrentDictionary<T,byte>并仅将其default(byte)用作值。


2

我更喜欢完整的解决方案,所以我做到了这一点:请注意,我的Count是用另一种方式实现的,因为我不明白为什么在尝试计算其哈希值时应禁止读取哈希集。

@Zen,感谢您的入门。

[DebuggerDisplay("Count = {Count}")]
[Serializable]
public class ConcurrentHashSet<T> : ICollection<T>, ISet<T>, ISerializable, IDeserializationCallback
{
    private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

    private readonly HashSet<T> _hashSet = new HashSet<T>();

    public ConcurrentHashSet()
    {
    }

    public ConcurrentHashSet(IEqualityComparer<T> comparer)
    {
        _hashSet = new HashSet<T>(comparer);
    }

    public ConcurrentHashSet(IEnumerable<T> collection)
    {
        _hashSet = new HashSet<T>(collection);
    }

    public ConcurrentHashSet(IEnumerable<T> collection, IEqualityComparer<T> comparer)
    {
        _hashSet = new HashSet<T>(collection, comparer);
    }

    protected ConcurrentHashSet(SerializationInfo info, StreamingContext context)
    {
        _hashSet = new HashSet<T>();

        // not sure about this one really...
        var iSerializable = _hashSet as ISerializable;
        iSerializable.GetObjectData(info, context);
    }

    #region Dispose

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
            if (_lock != null)
                _lock.Dispose();
    }

    public IEnumerator<T> GetEnumerator()
    {
        return _hashSet.GetEnumerator();
    }

    ~ConcurrentHashSet()
    {
        Dispose(false);
    }

    public void OnDeserialization(object sender)
    {
        _hashSet.OnDeserialization(sender);
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        _hashSet.GetObjectData(info, context);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    #endregion

    public void Add(T item)
    {
        _lock.EnterWriteLock();
        try
        {
            _hashSet.Add(item);
        }
        finally
        {
            if(_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public void UnionWith(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        _lock.EnterReadLock();
        try
        {
            _hashSet.UnionWith(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            if (_lock.IsReadLockHeld) _lock.ExitReadLock();
        }
    }

    public void IntersectWith(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        _lock.EnterReadLock();
        try
        {
            _hashSet.IntersectWith(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            if (_lock.IsReadLockHeld) _lock.ExitReadLock();
        }
    }

    public void ExceptWith(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        _lock.EnterReadLock();
        try
        {
            _hashSet.ExceptWith(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            if (_lock.IsReadLockHeld) _lock.ExitReadLock();
        }
    }

    public void SymmetricExceptWith(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            _hashSet.SymmetricExceptWith(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool IsSubsetOf(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.IsSubsetOf(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool IsSupersetOf(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.IsSupersetOf(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool IsProperSupersetOf(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.IsProperSupersetOf(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool IsProperSubsetOf(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.IsProperSubsetOf(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool Overlaps(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.Overlaps(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool SetEquals(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.SetEquals(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    bool ISet<T>.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.EnterWriteLock();
        try
        {
            return _hashSet.Contains(item);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        _lock.EnterWriteLock();
        try
        {
            _hashSet.CopyTo(array, arrayIndex);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

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

    public int Count
    {
        get
        {
            _lock.EnterWriteLock();
            try
            {
                return _hashSet.Count;
            }
            finally
            {
                if(_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            }

        }
    }

    public bool IsReadOnly
    {
        get { return false; }
    }
}

锁被释放了...但是内部哈希集如何处理,它的内存何时释放?
David Rettenbacher 2014年

1
@Warappa在垃圾回收时被释放。我唯一手动将事物清空并清除它们在类中的全部存在的时间是当主题包含事件并因此可能泄漏内存时(例如,当您使用ObservableCollection及其更改的事件时)。如果您可以增加我对该主题的理解,我会提出建议。我也花了几天时间研究垃圾收集,并对新信息一直感到好奇
Dbl 2014年

@AndreasMüller很好的答案,但是我想知道为什么您使用'_lock.EnterWriteLock();'其次是'_lock.EnterReadLock();' 在像“ IntersectWith”这样的方法中,我认为这里不需要读取外观,因为默认情况下,输入时写入锁定将阻止任何读取。
Jalal Said

如果您始终必须EnterWriteLock这样做,为什么还EnterReadLock存在呢?读取锁不能用于类似这样的方法Contains吗?
ErikE

2
这不是ConcurrentHashSet,而是ThreadSafeHashSet。请参阅我对@ZenLulz答案有关自我实现的评论。我99%确信使用这些实现的任何人都将在其应用程序中遇到严重的错误。
乔治·马夫里茨基斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.