【问题标题】:C# Access ConcurrentDictionary value by key in async appC# 在异步应用程序中按键访问 ConcurrentDictionary 值
【发布时间】:2020-01-22 12:21:41
【问题描述】:

我对 .NET C# 中的 ConcurrencyDictionary 有疑问。 我的应用程序将是异步的(我尝试这样做:))。

我有一些外部设备,它们通过一些 TCPIP 通信将数据发送到我的核心 (C# .NET)。我将对象存储在每个设备的 ConcurrentDictionary 值中。我对这些数据进行了一些操作,我需要在其中读取它并且有时会更改对象中的一些内容。

现在看起来不错,没有死锁(当我增加外部/模拟设备的数量时,它不会变慢,但它可以同时处理更多消息(并且不会丢失数据)

但是:我不确定我是否正确使用它。 我需要更改对象内部的一些值,调用一些函数并将所有更改存储在字典中。 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();
}

第二种方式看起来更加清晰和简单。但是我不确定我是否可以因为异步而做到这一点。

我会阻塞其他线程/任务吗?

哪种方式会更快?我期待第一个,因为在 DoJob 内部我不使用 dict,而是使用一些对象副本,毕竟我会更新 dict。

直接读取值(#2)会减慢整个过程吗?

即使在#2 方式中,其他进程是否可以从 dict 读取最后实现的值而没有任何麻烦?

当我打电话时会发生什么:

dict[myKey].DoSomeAnotherJob();

它是等待的,所以它不应该阻塞线程。但实际上它是在共享字典中调用的。

【问题讨论】:

    标签: c# .net multithreading dictionary asynchronous


    【解决方案1】:

    线程安全的ConcurrentDictionary(相对于普通的旧Dictionary)与异步/等待无关。

    这是做什么的:

    await dict[myKey].DoSomeAnotherJob();
    

    这是:

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

    您不需要ConcurrentDictionary 来调用该异步方法,dict 也可以是普通的Dictionary

    另外,假设MyClass 是一个引用类型(一个类而不是一个结构),将它的原始引用保存在一个临时变量中像你一样更新字典是不必要的。在您调用myClass.Prop1 = 10 之后,此更改会传播到您引用同一myClass 实例的所有其他地方。

    如果您想替换该值,您只想调用TryUpdate(),但您不这样做,因为它仍然是相同的引用 - 没有什么可以替换,myClass 和 @ 987654334@指向同一个对象。

    使用ConcurrentDictionary(而不是Dictionary)的唯一原因是从多个线程访问字典。所以如果你从不同的线程调用DoJob(),那么你应该使用ConcurrentDictionary

    另外,当涉及到多线程时,这是很危险的:

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

    因为与此同时,另一个线程可能会更改myKey 的值,这意味着每次调用dict[myKey] 时都会获得不同的引用。所以将它保存在一个临时变量中是要走的路。

    另外,使用索引器属性 (myDict[]) 而不是 TryGetValue() has its own issues,但仍然没有线程问题。

    【讨论】:

      【解决方案2】:

      两种方式在效果上是相等的。第一种方法使用从集合中读取的方法,第二种方法使用索引器来实现相同的目的。事实上,索引器在内部调用TryGetValue()

      当调用MyClass myClass = concurrentDictionary[key](或concurrentDictionary[key].MyClassOperation())时,字典在内部执行索引器属性的getter:

      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]
      

      除了如果 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 被设计为异步执行操作 UI 线程,await myClass.DoSomeAnotherJob() 不会阻塞。
      • TryGetValuethis[] 都不会阻塞其他线程
      • 两种访问变体的执行速度相同,因为它们共享相同的实现
      • dict[myKey].Operation()
        等于
        MyClass myclass = dict.[myKey]; myClass.Operation();


        GetMyClass().Operation()
        时也一样 等于
        MyClass myClass = GetMyClass(); myClass.Operation();

      • 你的看法是错误的。没有任何东西被称为字典的“内部”。从内部代码可以看出 sn -p 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;
        }
      }
      

      示例:假设字典在键“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;
        }
      }
      

      【讨论】:

      • 感谢两位的精彩回答。如果我理解正确,我现在不用担心异步应用程序,但如果我希望运行一些函数(例如我有 HTTP API,EmbedIO 在这里读/写 MyClass 中的一些属性)我将在单独的线程中运行,我必须在 MyClass 中使用 lock。
      • 正确。当MyClass 具有共享资源时,即每个线程可以修改属性的值(直接或通过方法),您必须同步此修改。使用lock 是一种解决方案。有MonitorSemaphoreSlim 等等。如果您想了解更多信息,请查看这个重要来源:Threading in C# by Joseph Albahari
      猜你喜欢
      • 2019-06-04
      • 1970-01-01
      • 1970-01-01
      • 2014-01-18
      • 2017-10-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多