首页 > 解决方案 > 仅当存在时,如何通过键原子更新 ConcurrentDictionary 中的值

问题描述

ConcurrentDictionary.TryUpdate 方法需要将 compareValue 与具有指定键的元素的值进行比较。但是,如果我尝试做这样的事情:

if (!_store.TryGetValue(book.Id, out Book existing))
{
    throw new KeyNotFoundException();
}

if (!_store.TryUpdate(book.Id, book, existing))
{
    throw new Exception("Unable to update the book");
}

当多个线程同时更新一本书时,它会引发异常,因为existing书在另一个线程中被更改。

我不能使用索引器,因为如果它不存在它会添加这本书,并且我无法检查密钥是否存在,因为它也不会是原子的。

我像这样更改了我的代码:

while (true)
{
    if (!_store.TryGetValue(book.Id, out Book existing))
    {
        throw new KeyNotFoundException();
    }

    if (_store.TryUpdate(book.Id, book, existing))
    {
        break;
    }
}

但我担心无限循环。

但是如果我对 Update 和 Delete 方法使用锁定,我将失去使用 ConcurrentDictionary 的优势。

解决我的问题的正确方法是什么?

标签: c#dictionarycollectionsconcurrentdictionary

解决方案


可以通过添加能够替换值的包装器来完成。为了简化代码,我将使用锁来实现这个包装器(以避免双值构造)。

首先 - 界面。请检查它是否反映了所需的操作。我使用int类型作为键和string值只是为了简化示例。

    public delegate TValue GetNewValue<TValue>(TValue previousValue);

    public interface IIntStringAtomicDictionary
    {
        /// <returns>true if was added, otherwise false</returns>
        bool AddIfMissingOnly(int key, Func<string> valueGetter);

        /// <returns>true if was updated, otherwise false</returns>
        bool UpdateIfExists(int key, GetNewValue<string> convertPreviousValueToNew);
    }

实现如下。它无法删除价值,可以简单地完成(如果需要,我可以更新答案)

    public sealed class IntStringAtomicDictionary : IIntStringAtomicDictionary
    {
        private readonly ConcurrentDictionary<int, ValueWrapper<string>> _innerDictionary = new ConcurrentDictionary<int, ValueWrapper<string>>();
        private readonly Func<int, ValueWrapper<string>> _wrapperConstructor = _ => new ValueWrapper<string>();

        public bool AddIfMissingOnly(int key, Func<string> valueGetter)
        {
            var wrapper = _innerDictionary.GetOrAdd(key, _wrapperConstructor);

            return wrapper.AddIfNotExist(valueGetter);
        }

        public bool UpdateIfExists(int key, GetNewValue<string> convertPreviousValueToNew)
        {
            var wrapper = _innerDictionary.GetOrAdd(key, _wrapperConstructor);

            return wrapper.AddIfExists(convertPreviousValueToNew);
        }
    }

    private sealed class ValueWrapper<TValue> where TValue : class
    {
        private readonly object _lock = new object();
        private TValue _value;

        public bool AddIfNotExist(Func<TValue> valueGetter)
        {
            lock (_lock)
            {
                if (_value is null)
                {
                    _value = valueGetter();

                    return true;
                }

                return false;
            }
        }

        public bool AddIfExists(GetNewValue<TValue> updateValueFunction)
        {
            lock (_lock)
            {
                if (!(_value is null))
                {
                    _value = updateValueFunction(_value);

                    return true;
                }

                return false;
            }
        }
    }

编写代码后,我们可以重新读取需求。据我了解,我们必须应用以下内容:

  • 不同的密钥应该是来自不同线程的更新而不加锁。
  • 值更新应该是原子的
  • 禁止并行增值 - 如有错误请说
  • 应该能够从不同的线程创建不同的值。

由于“并行增值”的限制,我们必须锁定价值创造。因此我上面的包装器有这个锁。

所有其他操作均不使用任何锁。

其他改进:

  • ValueWrapper类可以ReadWriteLockSlim用来允许并行读取值。
  • 可以使用相同的锁删除值。当然,我们可以在这里设置竞争条件。

推荐阅读