2014-12-04 3 views
1

У меня есть ConcurrentDictionary, который отображает простой тип в списке:Безопасное удаление отображение списка из ConcurrentDictionary

var dict = new ConcurrentDictionary<string, List<string>>(); 

я могу использовать AddOrUpdate() для удовлетворения как инициализации списка при добавлении первого значения и добавления последующих значений в список.

Однако это не относится к удалению. Если я что-то вроде:

public void Remove(string key, string value) 
{ 
    List<string> list; 
    var found = dict.TryGetValue(key, out list); 

    if (found) 
    { 
     list.Remove(value); 
     if (list.Count == 0) 
     { 
      // warning: possible race condition here 
      dict.TryRemove(key, out list); 
     } 
    } 
} 

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

Хотя я использую список в этом простом примере, у меня обычно есть ConcurrentBag или ConcurrentDictionary в таких сценариях, и риск очень похож.

Есть ли способ безопасного удаления ключа, когда соответствующая коллекция пуста, не прибегая к замкам?

+0

Is. есть также потенциальная гонка между вызовом 'TryGetValue' и' Remove'? – doctorlove

+0

Я мог ошибаться, но я так не думаю ... если кто-то добавит что-то перед проверкой пустоты, тогда он не будет пустым .. – Gigi

+0

Что делать, если они вызывают удаление? – doctorlove

ответ

2

Ваш ConcurrentDictionary защищен, но ваш список не является обязательным. Если к вашему списку можно получить доступ из нескольких потоков (я предполагаю, что это так), вам нужно использовать блокировку во всех доступах к списку или вам нужно использовать другую конструкцию.

После вызова TryGetValue в вашей Remove функции, Вы получите список несколько раз - так List<T> не является безопасным для многопоточности, вы рискуете различных проблем многопоточности.

Если вы используете вложенные ConcurrentDictionaries в dict, вы запускаете только проблему удаления чего-либо, что не является пустым. Как вы писали, возможно, что элемент добавлен во вложенный ConcurrentDictionary после проверки его размера. Удаление самого вложенного списка/словаря является потокобезопасным: содержащее dict - это ConcurrentDictionary, и оно будет безопасно обрабатывать элементы. Однако, если вы хотите гарантировать, что список/словарь удаляется только в том случае, если он пуст, вам необходимо использовать блокировку для всей операции.

Это связано с тем, что контейнер dict и вложенный список/словарь представляют собой две различные конструкции, а касание одной из них не влияет на другую - если вам нужна вся операция многоступенчатого атома, вам нужно убедиться только один поток может попытаться сделать это за раз.

Ваш код будет выглядеть примерно так:

if (found) 
{ 
    lock (_listLock) 
    { 
     list.Remove(value); 

     if (list.Count == 0) 
     { 
      // warning: possible race condition here 
      dict.TryRemove(key, out list); 
     } 
    } 
} 

Опять же, если вы используете незащищенную конструкцию (как List<T>, то вы должны использовать блокировку вокруг каждого доступа к этому списку

+0

Интересно, если замок должен быть немного выше - т. Е. До того, как установлено 'found' – doctorlove

+0

@doctorlove Я так не думаю (но я не хочу озвучивать это по-умолчанию). Когда 'TryGetValue' возвращается, вы держите ссылку на список, чтобы с ним ничего не могло случиться (оно не исчезнет и т. Д.). 'found' - локальная переменная, другие потоки не могут с ней справиться. Фактическая работа, которая должна быть атомарной, происходит внутри блока 'if', поэтому я думаю, что этого достаточно, чтобы заблокировать это. – xxbbcc