【问题标题】:Atomic AddOrUpdate for a C# DictionaryC# 字典的原子 AddOrUpdate
【发布时间】:2015-11-16 10:02:57
【问题描述】:

假设如下代码:

if (myDictionary.ContainsKey(aKey))
    myDictionary[aKey] = aValue;
else
    myDictionary.Add(aKey, aValue);

此代码访问字典两次,一次用于确定aKey 是否存在,另一次用于更新(如果存在)或添加(如果不存在)。我猜当这段代码只执行几次时,这种方法的性能是“可以接受的”。但是,在我的应用程序中,类似的代码大约执行了 500K 次。我分析了我的代码,它显示了 80% 的 CPU 时间花在这部分上(见下图),因此这激发了改进。

请注意,字典是lambdas

第一个解决方法很简单:

myDictionary[aKey] = aValue;

如果aKey 存在,则其值将替换为aValue;如果不存在,则将KeyValuePairaKey 作为键和aValue 作为值添加到myDictionary。但是,这种方法有两个缺点:

首先,您不知道aKey 是否存在,这会阻止您执行其他逻辑。例如,您不能基于此解决方法重写以下代码:

int addCounter = 0, updateCounter = 0;
if (myDictionary.ContainsKey(aKey))
{
    myDictionary[aKey] = aValue;
    addCounter++;
}
else
{
    myDictionary.Add(aKey, aValue);
    updateCounter++;
}

第二,更新不能是旧值的函数。例如,您不能执行类似以下的逻辑:

if (myDictionary.ContainsKey(aKey))    
    myDictionary[aKey] = (myDictionary[aKey] * 2) + aValue;    
else    
    myDictionary.Add(aKey, aValue);

第二种解决方法是使用ConcurrentDictionary。很明显,使用delegates 可以解决上述的问题;但是,我仍然不清楚如何解决 first 问题。

提醒一下,我关心的是加速。鉴于只有一个线程使用此过程,我不认为只有一个线程值得使用ConcurrentDictionary 的并发(带锁)惩罚。

我错过了一点吗?有人有更好的建议吗?

【问题讨论】:

  • 再次确认一下:这是单线程的,这里没有同步访问的理由吗?所以你实际上并不需要 atomic 操作,而只是一种快速设置值并确定你是添加还是更新值的方法?
  • 您可以检查Count 的项目在myDictionary[aKey] = aValue 之后是否在字典中发生了变化,以绕过第一个缺点。
  • 首先,将指定区域[_i]缓存在一个循环范围变量中,然后,将指定区域[_i].lambdas缓存在另一个循环范围变量中。你看到指定区域[_i].mu-- 中的 4,5% cpu 时间了吗?单独这样做可能会大大加快您的速度。
  • 我的意思是,在你的循环中,有一个 DegisnatedRegion currentRegion = _designatedRegions[_i]; 类型的变量。然后,有另一个变量 Dictionary currentLambdas = currentRegion.lambdas;我不是在谈论缓存,而是在本地将您处理的对象分配给变量,而不是通过索引器或属性获取器访问它们。仅从指定区域列表中获取指定区域就花费了 4.5%
  • @ghord 检查字典中是否存在键也是 O(1)。

标签: c# dictionary atomic


【解决方案1】:

如果您真的想要 ConcurrentDictionary 中的 AddOrUpdate 方法,但又不影响使用该方法的性能,您必须自己实现这样的 Dictionary。

好消息是,由于 CoreCLR 是开源的,您可以从 CoreCLR repository 获取实际的 .Net Dictionary 源并应用您自己的修改。好像不会那么难,看看那里的Insert私有方法。

一种可能的实现是(未经测试):

public void AddOrUpdate(TKey key, Func<TKey, TValue> adder, Func<TKey, TValue, TValue> updater) {

    if( key == null ) {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    }

    if (buckets == null) Initialize(0);
    int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
    int targetBucket = hashCode % buckets.Length;

    for (int i = buckets[targetBucket]; i >= 0; i = entries[i].next) {
        if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
            entries[i].value = updater(key, entries[i].value);
            version++;
            return;
        } 

    }
    int index;
    if (freeCount > 0) {
        index = freeList;
        freeList = entries[index].next;
        freeCount--;
    }
    else {
        if (count == entries.Length)
        {
            Resize();
            targetBucket = hashCode % buckets.Length;
        }
        index = count;
        count++;
    }

    entries[index].hashCode = hashCode;
    entries[index].next = buckets[targetBucket];
    entries[index].key = key;
    entries[index].value = adder(key);
    buckets[targetBucket] = index;
    version++;

}

【讨论】:

  • 我喜欢这个,因为它似乎是唯一的解决方案。另一个问题:我的应用程序将使用 GPLv3 开源,使用带有 MIT 许可证的 Microsoft 代码(尽管有修改)会违反任何规则吗?
  • @Hamed 我很确定you can,但我不是律师;)
  • 由于 .NET 6 有一种新方法可以做到这一点,请参阅我的其他答案
【解决方案2】:

.NET 6 开始,有一种新方法 CollectionsMarshal.GetValueRefOrAddDefault 可以做到这一点。

示例用法:

Dictionary<string, string> dictionary = new Dictionary<string, string>();

ref string? dictionaryValue = ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, "key", out bool exists);

//variable 'exists' is true if key was present, and false if it had to be added

if (exists)
{
    //Update the value of dictionaryValue variable
    dictionaryValue = dictionaryValue?.ToLowerCaseInvariant();
}
else
{
    //assign new value
    dictionaryValue = "test";
}


唯一的缺点是在调用此方法后您无法决定不添加新值。如果缺少键,它总是在您的字典中创建占位符空值。您基本上必须分配这个新值,否则您的字典中会留下一个空条目。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多