首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >并发HashSet

并发HashSet
EN

Code Review用户
提问于 2017-12-02 10:01:59
回答 1查看 7.8K关注 0票数 11

最近,我一直在使用HashSet并锁定每个方法,我发现这不仅是大量的工作(在很多地方都在使用),而且我开始发现代码中的不一致性。

后来我决定制作一个并发版本的HashSet,我之所以不使用ConcurrentDictionary版本是因为我只需要一个值,但是不能复制值,所以我也不能使用ConcurrentBag,我发现HashSet是我最好的选择,但是它缺少并发线程安全版本,这就是我的类出现的原因.

我对此有几个问题。主要是,这个实现线程安全吗?我有什么要担心的吗?我还能做些什么来让它变得更好吗?我是否正确地使用了一个ConcurrentHashSet,或者是否有人对一个集合有更好的想法?

最后一件事是,我自己编码的IEnumerator,而半睡着,因为原来的功能缺乏这一点,我需要使用它在foreach循环中。我很确定剩下的代码是线程安全的,但我想我是说我对我编写的代码不够自信,认为它是线程安全的,只是它似乎有点脱离了类的界面,我担心这可能会在某种程度上影响它。

IEnumerator:

代码语言:javascript
复制
public IEnumerator<T> GetEnumerator()
{
    _lock.EnterWriteLock();

    try
    {
        return _hashSet.GetEnumerator();
    }
    finally
    {
        if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
    }
}

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

完全实现:

代码语言:javascript
复制
public class ConcurrentHashSet<T> : IDisposable, IEnumerable<T>
{
    private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
    private readonly HashSet<T> _hashSet = new HashSet<T>();

    public IEnumerator<T> GetEnumerator()
    {
        _lock.EnterWriteLock();

        try
        {
            return _hashSet.GetEnumerator();
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

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

    public bool TryAdd(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 TryRemove(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();
            }
        }
    }

    public T FirstOrDefault(Func<T, bool> predicate)
    {
        _lock.EnterReadLock();

        try
        {
            return _hashSet.FirstOrDefault(predicate);
        }
        finally
        {
            if (_lock.IsReadLockHeld) _lock.ExitReadLock();
        }
    }

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

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _lock?.Dispose();
        }
    }
}
EN

回答 1

Code Review用户

发布于 2017-12-02 16:38:52

通用材料

最好在类和公共成员上有内联文档(///),但是一切都很简单和可以理解。有些人可能会质疑_hashSet这个名字,它没有告诉你它的目的,但是它是私人的,所以不是一个大的问题。

您正确地标识了可以使用ReaderWriterLock来控制访问,因为HashSet被记录为允许并发读取。这个类看起来线程安全,但GetEnumerator()除外。

IDisposable的实现看起来很好。

if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();

为什么你总是检查你是否持有锁?据我所知,这无济于事。如果您总是期望线程持有写锁,那么检查它是否被持有将防止在此期望被违反时发生剧烈崩溃,从而潜在地隐藏一个潜在的错误。这种“防御性”编程看起来很健壮,但是编写健壮的系统真的很困难,除非您对特定的故障有一个非常明确的设计目标,否则最好避免这样的防御性编码。一个愚蠢的例子是,如果您有一天意识到Contains(T)正在使用一个写锁而不是一个读锁(这个锁可能不会通过测试来显示),并相应地将它更改为,只有您忘记清除读锁而不是写锁,并且没有适当地释放它。如果试图释放写锁,就会崩溃,直接把您带到错误处,而不是让对象永久地被读锁。

关于那些不喜欢没有牙套的ifs的人的强制性评论。

GetEnumerator()

你对这种方法的怀疑是对的!HashSet提供的枚举器将导致在将其释放到中断枚举数中断枚举数之前发生的任何写入:“如果对集合进行了更改,如添加、修改或删除元素,则枚举数将不可恢复地失效,其行为未定义。”目前,它将只是崩溃,如果使用后,并发写,这是目前为止,我们可以希望的最好的结果!

当然,问题是当您检索枚举数时,锁定只排除写,但是枚举数必须在GetEnumerator()退出之后被枚举。

如何处理这是一个设计决定。您确实需要问自己为什么要实现IEnumerable<T>,并决定它是否真的是正确的。

  • 你可以写你自己的枚举器,它持有一个读锁.但是,您会发现一些建议,告诉您不要在网络上这样做,主要原因很简单:调用者可能不会使用枚举器,可能会错误地/部分地/懒散地使用它,或者可能不会释放它(就像foreach那样),所有这一切都意味着读锁可能会保持很长时间。这将提供一个“快速射击”。
  • 你可以拿一份套装并把它还给我。这将导致内存开销,但将是线程安全的,并再次提供一个snap-shot
  • 您可以停止实现IEnumerable,并提供允许“枚举”操作的单独方法(如您已经提供的FirstOrDefault )。这些'LINQy‘方法可以通过为使用者提供一个简单的(限制性的) API,允许对枚举进行受控处理,从而保证不再持有读锁。

--关于ConcurrentDictionary

的一点注记

ConcurrentDictionary有一个非平凡实现,并利用哈希表的特性来启用更好的并发性。对于一个生产环境,除非您能够展示使用ConcurrentSet的自定义实现而不是包装ConcurrentDictionary的价值,否则我强烈建议只包装ConcurrentDictionary,我们都希望它是线程安全的,甚至可能更高效(如果需要更多的空间来记录非值)

ConcurrentDictionaryGetEnumerator()GetEnumerator(),它只提供有限的保证(显然是文档),但是可以与写并发运行(因为它可以访问底层的数据结构)。但是,这并不是说您可以对它的枚举器做任何您想做的事情,因为它们可能保存对旧状态的引用,如果不释放,则可能代表内存泄漏。这并不能保证出现“快照”,因为来自某些写入的信息可能会在枚举中结束。同样,问题是为什么要进行枚举,并提供一个API来方便这一点。

票数 19
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/181832

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档