Как атомарно обновить значение в ConcurrentDictionary по ключу, только если оно существует - PullRequest
0 голосов
/ 10 октября 2018

Метод ConcurrentDictionary.TryUpdate требует, чтобы значение сравнения сравнивалось со значением элемента, имеющего указанный ключ.Но если я пытаюсь сделать что-то вроде этого:

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.

Как правильно решить мою проблему?

1 Ответ

0 голосов
/ 10 октября 2018

Это можно сделать, добавив оболочку, которая сможет заменить значение.Для упрощения кода я реализую эту обертку с блокировками (чтобы избежать создания двойного значения).

Прежде всего - интерфейс.Пожалуйста, проверьте, отражает ли это необходимые операции.Я использовал 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чтобы разрешить параллельное считывание значений.
  • Значения могут быть удалены с помощью одинаковых блокировок.Конечно, у нас могут быть условия гонки здесь.
...