C# Доступ к значению ConcurrentDictionary по ключу в приложении asyn c - PullRequest
1 голос
/ 22 января 2020

У меня есть вопрос о ConcurrencyDictionary в. NET C#. Мое приложение будет asyn c (я пытаюсь это сделать :)).

У меня есть несколько внешних устройств, которые отправляют данные в мое ядро ​​(C#. NET) через некоторое соединение TCPIP. Я храню объекты в значениях ConcurrentDictionary для каждого устройства. У меня есть некоторые операции с этими данными, где мне нужно прочитать их и иногда изменить некоторые в объекте.

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

Но: я не уверен, правильно ли я его использую. Мне нужно изменить некоторые значения внутри объекта, вызвать некоторые функции и сохранить все изменения в dict. Все объекты в dict должны быть доступны для чтения другими процессами (Я знаю, что во время «DoJob» другие процессы могут иметь старые значения в dict, пока я не сохраню значение, но в моем случае это нормально). Мне просто нужно избегать блокировки / блокировки других задач и делать это как можно быстрее.

Какой способ лучше:

1 способ (я использую его сейчас):

var dict = new ConcurentDictionary<MyClass>(concurrencyLevel, initalCapacity);

private async Task DoJob(string myKey)
{
    MyClass myClass;
    MyClass myClassInitState;
    dict.TryGetValue(myKey, out myClass);
    dict.TryGetValue(myKey, out myClassInitState);

    var value = myClass.SomeValueToRead;
    myClass.Prop1 = 10;
    await myClass.DoSomeAnotherJob();

    dict.TryUpdate(myKey, myClass, myClassInitState);
}

2 способ:

var dict = new ConcurentDictionary<MyClass>(concurrencyLevel, initalCapacity);

private async Task DoJob(string myKey)
{    
    var value = dict[myKey].SomeValueToRead;   
    dict[myKey].ChangeProp1(10);
    await dict[myKey].DoSomeAnotherJob();
}

Второй способ выглядит гораздо яснее и проще. Но я не уверен, что смогу сделать это из-за asyn c.

Буду ли я блокировать другие потоки / задачи?

Какой путь будет быстрее? Я ожидаю первого, потому что внутри DoJob я не работаю k с dict, но с некоторой копией объекта, и в конце концов я обновлю dict.

Может ли чтение значений напрямую (# 2) замедлить весь процесс?

Могут ли другие процессы читают последнее актуализированное значение из dict даже во время пути №2 без проблем?

Что происходит, когда я звоню:

dict[myKey].DoSomeAnotherJob();

Это ожидаемо, так что не должен блокировать темы. Но на самом деле он называется в общем dict в некоторой его стоимости.

Ответы [ 2 ]

1 голос
/ 22 января 2020

Поточно-безопасный ConcurrentDictionary (в отличие от простого старого Dictionary) не имеет ничего общего с async / await.

Что это делает:

await dict[myKey].DoSomeAnotherJob();

Это:

var temp = dict[myKey];
await temp.DoSomeAnotherJob();

Вам не нужен ConcurrentDictionary для вызова этого асинхронного c метода, dict также может быть обычным Dictionary.

Кроме того, предполагая, что MyClass является ссылочным типом (класс в отличие от структуры), сохраняя его исходную ссылку во временной переменной и , обновляя словарь, как вы это делаете, ненужным. В тот момент, когда вы позвонили myClass.Prop1 = 10, это изменение распространяется на все остальные места, где у вас есть ссылка на тот же экземпляр myClass.

Вы хотите позвонить TryUpdate(), только если хотите заменить значение, но вы этого не сделаете, так как это все та же ссылка - заменить нечего, оба myClass и myClassInitState указывают на один и тот же объект.

Единственная причина использовать ConcurrentDictionary (в отличие от Dictionary), это когда словарь доступен из нескольких потоков, Так что если вы вызываете DoJob() из разных потоков, тогда вам следует использовать ConcurrentDictionary.

Кроме того, когда задействована многопоточность, это опасно:

var value = dict[myKey].SomeValueToRead;   
dict[myKey].ChangeProp1(10);
await dict[myKey].DoSomeAnotherJob();

Поскольку в Между тем, другой поток может изменить значение для myKey, то есть вы получаете разные ссылки каждый раз, когда вызываете dict[myKey]. Поэтому сохранение его во временной переменной - это путь к go.

. Кроме того, использование свойства indexer (myDict[]) вместо TryGetValue() имеет свои собственные проблемы , но все же нет проблем с потоками.

0 голосов
/ 22 января 2020

Оба способа одинаковы в действии. Первый способ использует методы для чтения из коллекции, а второй способ использует индексатор для достижения того же. Фактически индексатор внутренне вызывает TryGetValue().

При вызове MyClass myClass = concurrentDictionary[key] (или concurrentDictionary[key].MyClassOperation()) словарь внутренне выполняет метод получения свойства индексатора:

public TValue this[TKey key]
{
  get
  {
    if (!TryGetValue(key, out TValue value))
    {
      throw new KeyNotFoundException();
    }
    return value;
  }     
  set
  {
    if (key == null) throw new ArgumentNullException("key");

    TryAddInternal(key, value, true, true, out TValue dummy);
  }
}

Внутренний код ConcurrentDictionary показывает, что

concurrentDictionary.TryGetValue(key, out value)

и

var value = concurrentDictionary[key]

одинаковы, за исключением того, что индексатор выдает KeyNotFoundException, если клавиша не существует.

Из точки потребления Просмотр первой версии с использованием TryGetValue позволяет писать более читаемый код:

// Only get value if key exists
if (concurrentDictionary.TryGetValue(key, out MyClass value))
{
  value.Operation();
}

против

// Only get value if key exists to avoid an exception
if (concurrentDictionary.Contains(key))
{
  MyClass myClass = concurrentDictionary[key];
  myClass.Operation();
}

Говоря о читабельности, ваш код может быть упрощен следующим образом:

private async Task DoJob(string myKey)
{
  if (dict.TryGetValue(myKey, out MyClass myClass))
  {
    var value = myClass.SomeValueToRead;
    myClass.Prop1 = 10;
    await myClass.DoSomeAnotherJob();    
  }
}

  • Поскольку async / await был разработан для асинхронного выполнения операции в потоке пользовательского интерфейса, await myClass.DoSomeAnotherJob() не будет блокироваться.
  • не будет TryGetValue или this[] блокировать другие потоки
  • оба варианта доступа выполняются с одинаковой скоростью, поскольку они используют одну и ту же реализацию
  • dict[myKey].Operation()
    равно
    MyClass myclass = dict.[myKey]; myClass.Operation();.

    То же самое, когда
    GetMyClass().Operation()
    равно
    MyClass myClass = GetMyClass(); myClass.Operation();

  • ваше восприятие неверно. Ничто не называется "внутри" словаря. Как видно из фрагмента внутреннего кода, dict [key] возвращает значение.

Замечания

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

Учтите, что следующий метод выполняется двумя потоками одновременно . Оба потока совместно используют один и тот же ConcurrentDictionary, содержащий, следовательно, общие объекты.

// Thread 1
private async Task DoJob(string myKey)
{
  if (dict.TryGetValue(myKey, out MyClass myClass))
  {
    var value = myClass.SomeValueToRead;
    myClass.Prop1 = 10;

    await myClass.DoSomeLongRunningJob(); 

    // The result is now '100' and not '20' because myClass.Prop1 is not thread-safe. The second thread was allowed to change the value while this thread was reading it
    int result = 2 * myClass.Prop1; 
  }
}

// Thread 2
private async Task DoJob(string myKey)
{
  if (dict.TryGetValue(myKey, out MyClass myClass))
  {
    var value = myClass.SomeValueToRead;
    myClass.Prop1 = 50;
    await myClass.DoSomeLongRunningJob(); 
    int result = 2 * myClass.Prop1; // '100'
  }
}

Также ConcurrentDictionary.TryUpdate(key, newValue, comparisonValue) аналогичен следующему коду:

if (dict.Contains(key))
{
  var value = dict[key];
  if (value == comparisonValue)
  {
    dict[key] = newValue;
  }
}

Пример : Допустим, словарь содержит элемент цифры c в ключе "Amount" со значением 50. Поток 1 хочет изменить это значение, только если поток 2 не изменил его за это время. Значение потока 2 является более важным (имеет приоритет). Теперь вы можете использовать метод TryUpdate, чтобы применить это правило:

if (dict.TryGetValue("Amount", out int oldIntValue))
{
  // Change value according to rule
  if (oldIntValue > 0)
  { 
    // Calculate a new value
    int newIntValue = oldIintValue *= 2;

    // Replace the old value inside the dictionary ONLY if thread 2 hasn't change it already
    dict.TryUpdate("Amount", newIntValue, oldIntValue);
  }
  else // Change value without rule
  {
    dict["Amount"] = 1;
  }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...