ConcurrentDictionary 的性能表现

3

我在解决这个问题上遇到了困难,非常感谢任何帮助。

我正在处理一个现有项目。我添加了逻辑来计算值的组合,确保我们不会超过某个限制。例如,给定这个数据表的列:
Name|Age|description
代码确保我们没有超过Name、Age的K种组合。我有包含百万对这样数据的数据。但是在某些情况下,程序会崩溃或卡住,虽然我没有看到任何内存问题或CPU问题。 我使用元组(Name,Age)作为键的ConcurrentDictionary实现了此限制,并且我正在使用C#.NET 6 ..
我可以看到尝试向DS添加元素所需的时间变得非常长。

编辑:添加一些代码片段,虽然这是很多内部实现,但我相信这些是理解问题的主要代码部分:

这是负责限制键的组件:

    protected override Result Process(Row row)
    {
        var valueToLimit = GetValueToLimit(row);
        var result = _values.TryAdd(valueToLimit);
        }
// some logic related to the case of crossing the limit
        return Result.Success;
    }

    protected abstract T GetValueToLimit(Row row);
}

对于我的情况,实现了函数GetValueToLimit:

protected override string[] GetValueToLimit(Row row)
{ // takes the relevant values from an input record, according to the requested columns. 
    return _columnIndices.Select(x => row.GetValue(x)).ToArray();
}

最后,这是并发HashSet实现的一些部分:

    public class BoundedConcurrentHashSet<K> : ConcurrentHashSet<K>
{
 ..
    public override Result TryAdd(K element)
    {
        if (Dictionary.Count() < _maxCapacity)
        {
            return base.TryAdd(element);
        }
        else
        {
            return Contains(element) ? Result.AlreadyInHash : Result.ExceedsCapacity;
        }
    }

使用C# ConcurrentDictionary 实现的ConcurrentHashSet:

public class ConcurrentHashSet<K>
{
    public ConcurrentHashSet(IEqualityComparer<K> equalityComparer)
    {
        Dictionary = new ConcurrentDictionary<K, object>(equalityComparer);
    }

    protected ConcurrentDictionary<K, object> Dictionary { get; }

    public int Count => Dictionary.Count;

    public IEnumerable<K> Elements => Dictionary.Keys;

    public virtual Result TryAdd(K element)
    {
        return Dictionary.TryAdd(element, null) ? dResult.Added : Result.AlreadyInHash;
    }

    public bool Contains(K element)
    {
        return Dictionary.ContainsKey(element);
    }

请分享任何可以帮助的想法。

谢谢


2
这个字典有什么价值?你能分享一些与这个字典交互的代码吗? - jalepi
@jalepi 值是字符串元组。我添加了代码。 - Nika
这个问题也与以下问题相关:具有上限的线程安全集合 - Theodor Zoulias
2个回答

3

这是你的问题:

public override ConcurrentHashSetAddResult TryAdd(K element)
{
    if (Dictionary.Count() < _maxCapacity)
    {
        return base.TryAdd(element);
    }
    //...

...其中Dictionary是底层的ConcurrentDictionary<K, object>对象。

Count()是一个LINQ方法,它要么从开始到结束枚举可枚举序列,要么返回Count属性,前提是序列实现了ICollection<TSource>接口。 ConcurrentDictionary<K, V>实现了此接口,因此确实使用了Count属性。这是此属性文档中的内容:

此属性具有快照语义,并表示在访问该属性时 ConcurrentDictionary<TKey,TValue> 中的项目数。

“快照语义”是重要的部分。这意味着为了获取“Count”,字典必须被完全锁定,暂时性地。当一个线程读取“Count”时,所有其他线程都必须等待。没有并发。
在GitHub上曾经提出过一个ApproximateCount属性,但它没有得到足够的关注,现在已经关闭。该属性将允许您使用大大减少的开销实现BoundConcurrentHashSet功能,但行为也不太准确:可能会超出_maxCapacity配置。
我的建议是放弃ConcurrentDictionary<K, object>,并使用一个带有lock保护的HashSet<T>作为底层存储。

非常感谢您的帮助!这听起来很合理,但我有一些问题:1.锁定不会让我们面临相同的问题吗? 2.我看到当DS变大时,计算时间会增加。这如何与.Count()成为问题相吻合?我希望它与字典大小无关。 - Nika
@NiaB 你可能想要研究 ConcurrentDictionary<K,V> 类的 源代码。简而言之,字典越大,它内部使用的锁就越多,Count 就越昂贵。该类旨在在添加、读取和删除特定键时快速且不具争议性。其他操作是为了完整性而支持的,不能保证它们是快速的。 - Theodor Zoulias
@NiaB HashSet<T>非常快,可以快速添加、读取和删除特定元素,并报告Count。因此,如果只在这些操作周围进行lock,除非每秒执行这些操作100,000次或更多,否则不太可能注意到很多争用。 - Theodor Zoulias
回顾一下,在多线程环境中使用 HashSet<T> 是否适用,这要看计算 T 的哈希码的成本。如果成本较小(例如 Tint),那么适用性会增加。如果成本较高(例如 T 是具有非常长字符串的 string),那么 HashSet<T> 可能会成为瓶颈。 - Theodor Zoulias
HashSet的键将是(string,string),其中每个字符串的长度限制为~16个字符。当从具有自己的比较器的两个字符串数组移动到元组(string, string)时,我确实看到了很大的性能提升。你有什么想法为什么会这样吗? - Nika

1

我发现在迭代和添加元素时使用普通集合,并在此过程中加锁,比使用并发集合要快得多。随着集合中元素的增加,这一点变得越来越明显。


1
使用lock枚举非线程安全的集合可能会很棘手,因为枚举大型集合可能需要相当长的时间,并且在此期间想要与集合交互的所有其他线程都将被阻塞。希望您不需要经常枚举它! - Theodor Zoulias

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接