【问题标题】:Multiple threads updating different items of a shared Dictionary with locks on keys多个线程通过键锁定更新共享字典的不同项目
【发布时间】:2017-01-05 15:35:23
【问题描述】:

我有一个Dictionary<string, List<MyObject>>,我需要在List<MyObject> 上运行一些资源密集型操作。我试图弄清楚是否可以让 Dictionary 的每个键都有一个线程来执行资源密集型任务,以便每个线程都更新其键的列表。也就是说,多个线程同时更新字典中的不同项目?

请考虑以下简化伪代码 -

public void MyMethod() {
    //The myDict object needs to be shared by all threads.
    Dictionary<string, List<MyObject>> myDict = new Dictionary<string, List<MyObject>>();
    //GetKeyValue() may return the same key multiple times
    foreach(var kv in GetKeyValue()) { 
        if(myDict.ContainsKey(kv.Key) { myDict[kv.Key].Add(kv.Value); }
        else { myDict.Add(kv.Key, kv.Value); }
        Task.Factory.StartNew(() => { RunSubsetSum(kv.Key, myDict); });
    }
}
//Resource intensive method
public void RunSubsetSum(string key, Dictionary<string, List<MyObject>> myDict)  { 
    //Lock on key so that no two threads run for the same key
    lock(key){
        foreach(var valueToRemove in GetRemovableObjs()) 
            myDict[kv.Key].Remove(valueToRemove);
    }
}

基本上,这个想法是-

  1. 没有两个线程同时为同一个键运行 - lock(key) 会使它们排队(按顺序运行)吗?
  2. 所有正在运行的线程都可以独立更新同一个字典 - 我希望多个线程能够同时更新同一个字典中的不同项目。

我尝试了上述方法,但结果似乎不一致。我认为这是因为 MyMethod() 更新了 RunSubsetSum() 已经在运行的键的字典,但不确定如何在不中断其他键循环的情况下锁定 MyMethod() 中的键。我想知道 C# 是否为这个问题提供了更简单的解决方案。有什么想法吗?

注意:我正在考虑创建一个字典,以便我可以跟踪当前正在处理哪些键并更新 MyMethod() 以缓冲键直到线程完成,但如果可以的话,我想避免添加它避免逻辑过于复杂。

【问题讨论】:

  • 你为什么不使用ConcurrentDictionary
  • @TimSchmelter 他不需要一个(字典只能从一个线程访问),这并不能解决他遇到的问题(字典中的值被多个线程访问) )。
  • @Servy:当然,他的想法是为字典中的每个键设置一个线程。但我的建议是使用ConcurrentDictionary 来避免这种情况。如果他访问列表,他需要锁定它
  • 再一次,字典首先不需要从多个线程访问,单个线程管理字典并提取然后由多个线程使用的值,所以没有必要对于ConcurrentDictionary,最重要的是,使用一个并不能解决不同线程访问多个值的问题。
  • 抱歉,伪代码没有反映出来,但字典对象应该由所有线程共享。不同线程使用同一个字典并可能同时且独立地更新自己的键的想法。

标签: c# multithreading


【解决方案1】:

您永远不应该在 string 上使用 lock。你只是在为一个受伤的世界敞开心扉,主要是围绕弦乐实习。每次您使用与另一个字符串文字在语义上相同的字符串文字时,它们将具有相同的引用(除非您将其关闭),这意味着如果您的任何字典键最终成为字符串文字,那么其他一些代码在某处else 在与您的代码无关的应用程序域中最终可能会锁定相同的值。这最终可能导致死锁,或者两个段在实际上不需要等待的时候等待。

您应该只在一个对象上lock,您可以确定只有管理同步的一种类型可以访问。这样做意味着您始终可以只查看这一类来分析正在进行的同步,并且您无需担心应用程序的其余部分发生了什么来确定此类的同步逻辑。

幸运的是,您已经确实拥有了一个对应于每个键的对象,并且您永远不会在此类之外公开List。你不需要单独的对象字典来锁定,你可以使用List

您遇到的另一个问题是您在工作线程中获取字典的值,这是不安全的,因为您正在另一个线程中对其进行修改,尽管通过简单地获取值可以轻松解决这个问题在开始新线程之前而不是之后,只需将stringList 传递给RunSubsetSum 而不是Dictionary

您还对来自工作线程和主线程的 List 对象进行了变异,因此您需要确保调用者在使用它之前锁定列表,以及工作人员。

【讨论】:

  • 谢谢。我可以摆脱对字符串键的锁定。如果我只在 RunSubsetSum 中传递字符串和 List,它会更新 Dictionary 中的 List 对象吗?基本上,我希望避免 RunSubsetSum 在它自己的 List 副本上工作,因为如此多的副本可能会进一步占用资源。这就是想要在同一个字典对象上运行所有线程的动机。
  • @Achilles 是的。 List 是一种引用类型,因此您传递的是引用的副本,而不是创建新列表。
  • 听起来不错,非常感谢。我将更新它以锁定列表。如何修改MyMethod(),使其不更新已运行线程的字典项?如果我只是对if(myDict.ContainsKey(kv.Key) .... 部分应用相同的锁,它会解决问题吗?保持foreach(var kv in GetKeyValue()) 循环运行是有意义的,只是不要让它更新正在运行的线程的项目。
  • @Achilles 你需要在List 的所有突变周围使用lock,仅此而已。
  • 知道了。如果我在循环中应用锁,它会阻塞循环还是保持循环继续,同时将锁定的部分保留在某个队列中?
【解决方案2】:

锁定字符串不会帮助同步资源。由于字符串是不可变的对象。每次,将字符串传递给函数都会导致新的字符串。更重要的是,这两种方法都在同时接触字典。建议将它们结合起来,然后根据您的需要拨打电话。

private readonly object _padLock = new object();
public void CallingMethod() {
   Task.Factory.StartNew(() => { MyMethod(); })
}

public void MyMethod() {
   lock (_padLock)
   {
      //The myDict object needs to be shared by all threads.
      Dictionary<string, List<MyObject>> myDict = new Dictionary<string, List<MyObject>>();
      //GetKeyValue() may return the same key multiple times
      foreach(var kv in GetKeyValue()) { 
        if(myDict.ContainsKey(kv.Key) { myDict[kv.Key].Add(kv.Value); }
        else { myDict.Add(kv.Key, kv.Value); }
        RunSubsetSum(kv.Key, myDict);
      }
   }
}
//Resource intensive method
public void RunSubsetSum(string key, Dictionary<string, List<MyObject>> myDict)  { 
    //Lock on key so that no two threads run for the same key
        foreach(var valueToRemove in GetRemovableObjs()) 
            myDict[kv.Key].Remove(valueToRemove);
}

【讨论】:

  • 是的,string 是不可变的,但是不,将 string 传递给函数不会创建新字符串。它将引用的副本传递给字符串,因此它将具有相同的引用。正如我在回答中提到的那样,锁定string 还有其他问题,但它们完全不同。至于您的其余更改,您已将代码从根据规定的要求在其自己的线程中处理每个值更改为简单地同步整个事物并且根本不使用任何额外的线程,这根本不是什么被要求。
【解决方案3】:

Servy 关于锁定字符串是正确的。简单的解决方案是创建一个私有字段仅用于锁定:

object LockMe = new object();

public void SomeMethod()
{
    lock(LockMe)
    {
        <... do something here ...>
    }
}

另一个问题是要记住,每个应用程序有一个最大允许线程数,因此,如果您为字典中的每个键创建一个线程,您就有达到最大线程数的风险。

您可能需要重新考虑您的线程模型。

【讨论】:

  • 谢谢。锁定字符串键的原因是为了确保没有两个线程在同一个字典项上工作。如果我们锁定一个私有字段,它会锁定整个字典,对吗?那么,不再需要并行处理多个字典项了吗?
  • @Achilles 是的,除非您在问题中提到每个键值都有一个对象。
  • 啊,是的。你是对的。我没想到。每个键都需要一个单独的对象实例。
猜你喜欢
  • 2018-06-13
  • 2010-12-25
  • 2023-04-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-12
  • 2017-02-07
  • 1970-01-01
相关资源
最近更新 更多