如何仅在密钥存在时通过密钥对ConcurrentDictionary中的值进行原子更新

God*_*lla 7 c# collections dictionary concurrentdictionary

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

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");
}
Run Code Online (Sandbox Code Playgroud)

当多个线程同时更新一本书时,它会抛出一个异常,因为existingbook在另一个线程中被更改了.

我不能使用索引器,因为它添加了书,如果它不存在,我不能检查键是否存在,因为它也不会原子.

我改变了我的代码:

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

    if (_store.TryUpdate(book.Id, book, existing))
    {
        break;
    }
}
Run Code Online (Sandbox Code Playgroud)

但我担心无限循环.

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

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

Man*_*gor 1

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

首先——界面。请检查它是否反映了所需的操作。我使用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);
    }
Run Code Online (Sandbox Code Playgroud)

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

    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;
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

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

  • 不同的密钥应该从不同的线程更新,而不需要锁定。
  • 值更新应该是原子的
  • 如果禁止并行增值 - 请指出是否错误
  • 应该能够从不同的线程创建不同的值。

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

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

其他改进:

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