Добавление переменных значений в список внутри параллельного словаря

#c# #multithreading #linq #thread-safety

#c# #многопоточность #linq #потокобезопасность

Вопрос:

У меня есть десятичные дроби, которые я пытаюсь добавить в список внутри ConcurrentDictionary

 ConcurrentDictionary<string, List<decimal>> fullList =
    new ConcurrentDictionary<string, List<decimal>>();

public void AddData(string key, decimal value)
{
    if (fullList.ContainsKey(key))
    {
        var partialList = fullList[key];
        partialList.Add(value);
    }
    else
    {
        fullList.GetOrAdd(key, new List<decimal>() { value });
    }
}
  

Технически приведенный выше код работает для меня, но это было сделано только так, потому что я не знал, как выполнить GetOrAdd метод как для добавления, так и для обновления. Мой вопрос в том, как мне использовать этот метод для обоих, учитывая, что мое обновление будет заключаться в добавлении элемента в конец существующего списка?

Комментарии:

1. Имейте в виду, что AddData это не потокобезопасно. Если два потока одновременно вызывают его с одним и тем же key значением, поведение будет неопределенным. List Класс не является потокобезопасным.

2. @TheodorZoulias чтобы быть полностью потокобезопасным, мне нужно было бы создать ConcurrentDictionary<string, ConcurrentBag<decimal>> вместо этого?

3. Да. Или даже лучше ConcurrentDictionary<string, ConcurrentQueue<decimal>> . Это ConcurrentBag очень специализированный класс (он хорош только в смешанных сценариях производитель-потребитель). Люди часто думают, что это самый близкий тип к потокобезопасному List , но это не так. Для справки: когда использовать потокобезопасную коллекцию

4. @TheodorZoulias Я новичок в многопоточном аспекте в C #, как вы, вероятно, можете сказать, но я должен указать, что я добавляю в словарь в одном потоке и читаю из него в другом, поэтому я не уверен, в какой сценарий это меня поставит. Вы все еще рекомендуете параллельную очередь поверх параллельного пакета?

5. Да, ваш сценарий является чистым производителем-потребителем, а не смешанным, поэтому ConcurrentQueue он будет работать лучше, чем ConcurrentBag . Смешанные сценарии производитель-потребитель, в которых один и тот же поток действует и как производитель, и как потребитель, встречаются очень редко. Лично я никогда не видел и не нуждался в нем.

Ответ №1:

AddOrUpdate Для этой цели вы можете использовать метод, как показано ниже:

 fullList.AddOrUpdate(key, 
                     new List<decimal>() { value }, 
                     (key,list) => 
                     { 
                         list.Add(value); 
                         return list; 
                     });
  

По сути, когда key отсутствует, создается новый список только с одним элементом, value . В противном случае мы добавляем в список, связанный с текущим key , value и возвращаем список.

Для получения документации по этому методу, пожалуйста, посмотрите здесь.

Ответ №2:

Вы ищете что-то подобное? Надеюсь, это поможет.

     ConcurrentDictionary<string, List<decimal>> fullList = new ConcurrentDictionary<string, List<decimal>>();
    public void AddData(string key, decimal value)
    {
        List<decimal> list;
        if (!fullList.TryGetValue(key, out list))
        {
            list = new List<decimal>();
            fullList.GetOrAdd(key, list);
        }
        list.Add(value);
    }
  

Ответ №3:

Вам просто нужно вызвать GetOrAdd во всех случаях:

 public void AddData(string key, decimal value)
{
    var partialList = fullList.GetOrAdd(key, _ => new List<decimal>());
    lock (partialList) partialList.Add(value); // Lock for thread safety
}
  

Когда вы используете a ConcurrentDictionary , вы должны стремиться к его специальному параллельному API, который обеспечивает атомарность операций. Проверка и добавление в два этапа ( ContainsKey Add ) не являются атомарными, они вводят условие гонки и могут поставить под угрозу корректность вашего приложения.