【问题标题】:Can't safely lock a value of a ConcurrentDictionary无法安全地锁定 ConcurrentDictionary 的值
【发布时间】:2010-10-26 15:51:27
【问题描述】:

我无法锁定 Collection 中的项目 - 特别是 ConcurrentDictionary。

我需要接受一条消息,在字典中查找该消息,然后对其进行长时间扫描。由于程序占用大量内存,扫描后对象返回 true 如果他们认为是删除它的好时机(我通过从字典中删除它来做到这一点)。但是,另一个线程可能会在类似的时间出现,并在删除后立即尝试访问同一个对象。这是我的第一次尝试:

string dictionaryKey = myMessage.someValue;

DictionaryObject currentObject = myConcurrentDictionary.GetOrAdd(dictionaryKey, new DictionaryObject());
// we can be interrupted here
lock (currentObject)
{
    //KeyNotFoundException is possible on line below
    if (myConcurrentDictionary[dictonaryKey].scan(myMessage)) // Scans the message - returns true if the object says its OK to remove it from the dictionary
    {
      DictionaryObject temp;                      //   It's OK to delete it
      if (!queuedMessages.TryRemove(ric, out temp))   // Did delete work?
       throw new Exception("Was unable to delete a DictionaryObject that just reported it was ok to delete it");
    }
}

但是,上述方法不起作用 - 一个线程可能会在另一个线程尝试访问字典中的对象之前从字典中删除一个对象。在阅读了lock is shorthand for Monitor.Enter and Monitor.Exit 之后,我尝试了这个:

string dictionaryKey = myMessage.someValue;
Monitor.Enter(GetDictionaryLocker);
DictionaryObject currentObject = myConcurrentDictionary.GetOrAdd(dictionaryKey, new DictionaryObject());
// we can be interrupted here
lock (currentObject)
{
    Monitor.Exit(GetDictionaryLocker);
    //KeyNotFoundException is still possible on line below
    if (myConcurrentDictionary[dictonaryKey].scan(myMessage)) // Scans the message - returns true if the object says its OK to remove it from the dictionary
    {
      DictionaryObject temp;                   //   It's OK to delete it
      if (!queuedMessages.TryRemove(ric, out temp))   // Did delete work?
       throw new Exception("Was unable to delete a DictionaryObject that just reported it was ok to delete it");
    }
}

尝试在字典中查找对象时,这两种方法都可能导致 KeyNotFoundException

有谁知道我怎样才能找到我想锁定的对象,然后将其锁定而不会被打断?抱歉 - 我是并发新手,感到非常困惑!

谢谢,

弗雷德里克

【问题讨论】:

  • 我认为您不必为 ConcurrentDictionary 添加自定义锁定?它是线程安全的吗?
  • Hi Dismissile,是的,它是线程安全的,但是查找我想要的密钥然后实际获取我的锁的操作不是。
  • @Dissimilie - 你是对的,这里的问题是没有办法自动检索对象并将其标记为“正在处理”以避免并发scan()s。
  • 也许你会选择ConcurrentQueue<>ConcurrentDicrionary<> 适合映射。所以出列下一个工作(处理)项目并处理它。如果您在处理之前需要一些映射,然后映射工作项(但它已经超出处理队列,所以没有并发问题)

标签: c# .net multithreading concurrency c#-4.0


【解决方案1】:

您应该在开始扫描之前从字典中删除该对象,以防止任何其他线程尝试同时使用它。在scan() 失败后,如果您以后必须重新添加它,您可以随时添加它。在此并发集合上,删除和添加都保证是线程安全的。

这应该使您想要的成为可能,而无需任何 locks 或 Monitor 使用。

string dictionaryKey = myMessage.someValue;

DictionaryObject currentObject = null;
if (myConcurrentDictionary.TryRemove(dictionaryKey, out currentObject))
{
    //KeyNotFoundException is possible on line below
    if (!currentObject.scan(myMessage)) // Scans the message - returns true if the object says its OK to remove it from the dictionary
    {
      if (!myConcurrentDictionary.TryAdd(dictionaryKey, currentObject))
       throw new Exception("Was unable to re-insert a DictionaryObject that is not OK for deletion");
    }
} 

在不了解您的其余代码的情况下,我对此的担忧是,在您调用 scan() 期间,是否有其他线程可以使用相同的键添加回另一条消息。这将导致TryAdd 失败。如果这是可能的,还需要做更多的工作。

您当前模型的问题在于,即使集合是线程安全的,如果您希望将“正在扫描”的项目留在集合中,您真正必须做的是执行以下操作组合 原子:1. 找到一个免费项目,2. 将其标记为“正在使用”。

  1. 可以通过线程安全的集合来完成,但是
  2. 必须单独完成,因此您为同一对象上的多个scan()s 打开一个窗口。

【讨论】:

  • 感谢史蒂夫的回复。你是对的 - 首先删除一个项目听起来很优雅。不过,我迷失了如何告诉其他线程该对象正在被处理,而不仅仅是创建一个新对象。有什么建议吗?
  • @Frederik 你可以去@supercat 的方式
  • @Frederik - 一旦你解决了这个问题,我会提出一个新问题。这里没有足够的信息来可靠地回答这个问题。我在对我的回复的编辑中指出了我对这个主题的关注(在代码之后)。您可以在找到候选对象时恢复为简单的Dictionary<>lock(),并更新对象中的状态字段以将其标记为“正在扫描”。在这种情况下,在scan() 中处理之前,其他可能的消费者必须检查对象状态(也在Dictionary<> 上的lock() 内)。在这里,您可以将对象留在原处。
  • @supercat 的响应还显示了一种在不放弃 ConcurrentDictionary 的情况下检查对象状态的可能方法。请注意,这可能会导致您在使用中的对象上反复尝试并失败。
  • 史蒂夫,我仍然有点困惑,但感谢您的建议 - 您给了我很多帮助。也感谢其他所有人。
【解决方案2】:

我认为这里对“锁定”对象存在根本性的误解。

当您对某个对象 (Monitor.Enter) 持有锁时,它实际上并不会阻止其他线程使用该对象。它只是防止其他线程在该特定对象上获得锁定。您需要做的是添加一个辅助对象,并锁定它 - 并确保每个线程也锁定它。

话虽如此,这将引入同步开销。可能值得尝试设计一种仅在存在冲突时才使用锁的设计——例如在扫描之前实际移除对象,然后锁定,并在扫描期间保持锁定。其他线程只有在他们请求的对象不是字典的一部分时才能尝试获取锁...

【讨论】:

    【解决方案3】:

    这是猜测……

    问题是concurrentDictionary返回Unique对象给每个线程,防止它们互相干扰。

    任何添加、更改或删除方法都将确保访问正确的项目,但锁只会锁定您的本地包装器。

    任何解决方案都取决于您可以用字典做什么。

    线程是否可以直接从字典中提取消息并仅在扫描后替换它,或者您是否可以使用主线程遍历字典并将所有消息添加到线程从中提取的队列对象以及处理删除文档之后应该删除。

    这样每个线程将永远不会竞争相同的消息。

    【讨论】:

    • 谢谢你 - 很好的建议。你,Reed 和 supercat 提到的删除项目优先策略听起来很优雅,但我不确定我将如何处理另一个线程在字典中查找该项目但没有找到它 - 然后它会想要创建一个新的副本对象 - 另一个已经“检查出来”,它应该等待另一个线程。这有意义吗?
    • @Frederik,为什么其他线程带有类似的message。如果必须处理消息,那么它必须如何与以前的堆叠?你如何解决冲突,即当消息处理开始但另一个类似的消息到达时?处理如何修改DictionaryObject的状态
    • 也许是另一种选择,使用队列将消息随机发送到工作线程。这样,每个线程将获得单独的消息,不会发生冲突,并且字典仍将包含所有消息,直到它们被识别为要删除。
    【解决方案4】:

    如何在对象中包含一个字段来指示哪个线程拥有它,并使用 Threading.Interlocked.CompareExchange 尝试获取它?假设如果一个对象正在使用中,代码不应该锁定而只是放弃操作。

    【讨论】:

    • 我以前不熟悉 Threading.Interlocked.CompareExchange - 谢谢。不幸的是,放弃操作不是一种选择——因为消息必须以正确的顺序到达。
    • @Frederik,您能否在问题中描述您需要实现哪些处理逻辑?也许您会将Queue<> 实现为ConcurrentDictionary<> 值并在那里收集处理对象。或者您的DictionaryObject 必须共享?
    • 如果您想处理它们以便查看BlockingCollection<T> - msdn.microsoft.com/en-us/library/dd267312.aspx。当使用一个线程提供服务时,这应该保证顺序,至少在它们以正确的顺序添加到队列的情况下。
    • @Frederik:因此,您需要确保当其他人想要添加某个项目时正在扫描该项目,或者该项目将被删除并创建一个新项目,或者该项目将被附加而不是被删除。最好的可能是结合使用 CompareExchange 和锁定;您需要在实际删除每个项目时使用锁定它,并使用 CompareExchange 来跟踪是否正在删除项目,以及在扫描期间是否已经或正在尝试添加到项目。如果在删除期间尝试添加到项目,请等待旧项目的锁定,然后重试。
    猜你喜欢
    • 1970-01-01
    • 2012-08-29
    • 2012-09-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-29
    • 1970-01-01
    相关资源
    最近更新 更多