【问题标题】:Safely removing list mapping from ConcurrentDictionary从 ConcurrentDictionary 中安全删除列表映射
【发布时间】:2014-12-04 15:07:47
【问题描述】:

我有一个 ConcurrentDictionary,它将一个简单类型映射到一个列表:

var dict = new ConcurrentDictionary<string, List<string>>();

我可以使用AddOrUpdate() 来满足添加第一个值时列表的初始化以及将后续值添加到列表中。

但是,移除的情况并非如此。如果我这样做:

public void Remove(string key, string value)
{
    List<string> list;
    var found = dict.TryGetValue(key, out list);

    if (found)
    {
        list.Remove(value);
        if (list.Count == 0)
        {
            // warning: possible race condition here
            dict.TryRemove(key, out list);
        }
    }
}

...如果相应的列表不再有任何值,我的意图是完全删除键(在概念上类似于引用计数),那么我冒着竞争条件的风险,因为可能有人在我检查列表是否为空后立即将其添加到列表中

虽然我在这个简单的例子中使用了一个列表,但在这种情况下,我通常有一个 ConcurrentBag 或 ConcurrentDictionary ,并且风险非常相似。

当相应的集合为空时,有什么方法可以安全地移除密钥,而不是求助于锁?

【问题讨论】:

  • 调用TryGetValueRemove 之间是否也存在潜在的竞争?
  • 我可能是错的,但我不这么认为...如果有人在空性检查之前添加了一些东西,那么它就不会是空的...
  • 如果他们调用remove怎么办?
  • 如果同时删除两件事,然后其中一个发现列表为空并将其删除,这是预期的行为。真正的危险是由于竞争条件而丢失了一个非空列表。

标签: c# concurrency race-condition concurrentdictionary


【解决方案1】:

您的ConcurrentDictionary 受到保护,但您的列表不受保护。如果可以从多个线程访问您的列表(我假设是这种情况),您需要对所有对列表的访问使用锁定,或者您需要使用不同的构造。

在您的Remove 函数中调用TryGetValue 后,您会多次访问该列表 - 因为List&lt;T&gt; 对多线程不安全,您将面临各种线程问题的风险。

如果您在dict 中使用嵌套的 ConcurrentDictionaries,您只会遇到删除非空内容的问题 - 正如您所写的,在您检查项目的大小后,可能会将项目添加到嵌套的 ConcurrentDictionary 中。删除嵌套列表/字典本身是线程安全的:包含dictConcurrentDictionary,它将安全地处理删除项目。但是,如果你想保证列表/字典只有在它为空时才被删除,你必须对整个操作使用锁。

这是因为容器 dict 和嵌套列表/字典是两种不同的结构,触摸一个对另一个没有影响 - 如果您需要整个多步骤操作是原子的,您必须使确保一次只有一个线程可以尝试这样做。

你的代码应该是这样的:

if (found)
{
    lock ( _listLock )
    {
        list.Remove(value);

        if (list.Count == 0)
        {
            // warning: possible race condition here
            dict.TryRemove(key, out list);
        }
    }
}

同样,如果您使用的是不受保护的构造(例如 List&lt;T&gt;,那么您必须在对该列表的每次访问时使用锁定。

【讨论】:

  • 我想知道锁是否需要更高一点 - 即在设置 found 之前
  • @doctorlove 我不这么认为(但我并不是要在这方面听起来很权威:))。一旦TryGetValue 返回,您将持有对列表的引用,因此不会发生任何事情(它不会消失,等等)。 found 是一个局部变量,其他线程不能乱用它。应该是原子的实际工作发生在 if 块内,所以我认为锁定它就足够了。
  • 如果您使用lock 保护TryRemove,那么您必须在每次访问并发字典时使用相同的lock。否则这个lock 将一事无成。
  • @TheodorZoulias 我不相信在这种情况下ConcurrentDictionary 需要锁。锁定到位是因为列表实例已被修改,并且同一列表可能同时在另一个线程中被修改。从字典中获取/删除列表实例不需要锁; TryRemove() 调用位于锁内,因为 if() 作用于必须保护的列表实例。你觉得我的推理有问题吗?
  • 我猜项目可以同时从另一个线程插入到列表中。如果您不使用相同的锁保护插入项目的代码,则可以在 if (list.Count == 0) 行之后和 dict.TryRemove(key, out list) 行之前添加一个项目。在这种情况下,将从字典中删除一个非空列表。不是问题吗?
猜你喜欢
  • 1970-01-01
  • 2018-07-08
  • 2021-05-01
  • 1970-01-01
  • 1970-01-01
  • 2012-04-19
  • 2021-09-16
  • 2019-12-15
  • 2013-05-30
相关资源
最近更新 更多