Это надежный способ использования AddOrUpdate в ConcurrentDictionary?

Я следил за курсом Pluralsight, автором которого является Саймон Робинсон в Concurrent Collections.

Он использует AddOrUpdate следующим образом, чтобы сделать его потокобезопасным:

public bool TrySellShirt(string code)
{
    bool success = false;

    _stock.AddOrUpdate(code, 
        (itemname) => { success = false; return 0; },
        (itemName, oldValue) =>
        {
            if (oldValue == 0)
            {
                success = false;
                return 0;
            }
            else
            {
                success = true;
                return oldValue - 1;
            }
        });

    if (success)
        Interlocked.Increment(ref _totalQuantitySold);

    return success;
}

Итак, я знаю, что AddOrUpdate не является полностью атомарным, как сказано в документации: делегаты addValueFactory и updateValueFactory вызываются вне блокировок, чтобы избежать проблем, которые могут возникнуть при выполнении неизвестного кода под блокировкой.< /эм>

Это мне понятно. Что не ясно, так это какой смысл устанавливать success в false в делегатах. Аргумент AddValueFactory намеренно используется как лямбда, поэтому можно установить success = false, а не просто возвращать 0. Я немного понимаю/думаю, что если метод/лямбда прерывается другим потоком (и он может быть прерван, потому что он вызывается вне блокировки), он попытается повториться, поэтому мы должны установить состояние любых соответствующих значений на их начальное значение, чтобы четко включить новую итерацию, поэтому установите success = false;.

Также из документации: Если вы вызываете AddOrUpdate одновременно в разных потоках, addValueFactory может вызываться несколько раз, но его пара ключ/значение может не добавляться в словарь при каждом вызове.

Если это так, я проверял исходный код AddOrUpdate на source.dot.net, я не вижу нигде используемых блокировок, я вижу TryAddInternal и TryUpdateInternal.

Тем не менее, описанный выше метод работает, но я не понимаю, почему он работает, и как только я удаляю, казалось бы, ненужные success = false назначения, он не работает, возникает несоответствие. Так что мне любопытно, что заставляет этих делегатов повторяться после неудачи?

У меня есть вопросы:

1. Безопасно ли использовать AddOrUpdate, как показано, или я должен просто заблокировать все и забыть об этом?

2. Что заставляет делегатов повторяться после того, как их прервали? Имеет ли это какое-либо отношение к «Сравнить и поменять местами»? (самое интересное в этом);

3. Есть ли какие-либо темы/концепции, которые вы хотели бы, чтобы я проверил, чтобы лучше понять потокобезопасную среду?


person Sha Kal    schedule 27.04.2021    source источник


Ответы (1)


Поскольку делегаты addValueFactory и updateValueFactory вызываются ConcurrentDictionary без каких-либо блокировок, другой поток может изменить содержимое словаря, пока выполняется код add/updateValueFactory. Чтобы справиться с этим сценарием, если был вызван addValueFactory (поскольку ключ не существует в словаре), он добавит возвращаемое значение только в том случае, если ключ все еще не существует в словаре. Точно так же, если был вызван updateValueFactory, он обновит значение для ключа только в том случае, если текущее значение по-прежнему равно oldValue.

Если есть несоответствие в результате добавления/обновления/удаления того же ключа другим потоком во время выполнения кода add/updateValueFactory, он просто попытается снова вызвать соответствующий делегат на основе последнего содержимого словаря (делегаты не прервано, и это сам словарь вызывает их снова, значение для добавляемого/обновляемого ключа изменилось). Это объясняет, почему вам по-прежнему нужны назначения success = false в лямбда-выражениях, хотя success инициализировано значением false. Следующий пример может помочь визуализировать поведение:

Исходное состояние словаря: _stock["X"] = 1

Step Thread 1 Thread 2
1 Calls _stock.AddOrUpdate("X", ...)
2 updateValueFactory invoked (oldValue = 1)
3 Calls _stock.AddOrUpdate("X", ...)
4 updateValueFactory invoked (oldValue = 1)
5 Sets success = true, returns oldValue - 1 = 0
6 Dictionary checks that the value for key "X" is still = 1 (true)
7 Value for key "X" is updated to 0
8 Sets success = true, returns oldValue - 1 = 0
9 Dictionary checks that the value for key "X" is still = 1 (false)
10 updateValueFactory invoked again (oldValue = 0)
11 Sets success = false, returns 0
12 Dictionary checks that the value for key "X" is still = 0 (true)
13 Value for key "X" is updated to 0
14 Final value of success is false Final value of success is true

Обратите внимание, что без явной установки success = false в ветви oldValue == 0 ветви if поток 1 подумал бы, что он все еще успешно продал товар, даже если запаса больше не было, потому что поток 2 продал последний.

Таким образом, техника в вашем вопросе работает по назначению.

person Iridium    schedule 27.04.2021
comment
Спасибо за разъяснение, 1. Безопасно ли использовать AddOrUpdate, как показано, или я должен просто заблокировать все и забыть об этом? - person Sha Kal; 27.04.2021
comment
@ShaKal - да, кажется безопасным использовать AddOrUpdate таким образом, если вы тщательно реализуете его, как в вашем примере (будьте уверены, что не пропустите какие-либо назначения success = false). - person Iridium; 27.04.2021