仅当键存在时如何通过键自动更新ConcurrentDictionary中的值

时间:2018-10-10 16:58:21

标签: c# dictionary collections 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的优势。

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

2 个答案:

答案 0 :(得分:0)

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

首先-界面。请检查它是否反映了所需的操作。为了简化示例,我将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允许并行读取值。
  • 可以使用相同的锁删除值。当然,我们可以在这里获得比赛条件。

答案 1 :(得分:-1)

一种可能的解决方案是接受空值:

if (!_store.AddorOUpdate(book.Id, null, (k, v) => book))
{
    throw new Exception("Unable to update the book");
}

尽管有一些警告:

  • 仅仅存在一个键以了解一本书是否在字典中是不够的,但是其值必须不是null
  • 使用无用键来增加字典的大小。