#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
) не являются атомарными, они вводят условие гонки и могут поставить под угрозу корректность вашего приложения.