8

考虑以下代码:

Dictionary<string, string> list = new Dictionary<string, string>();
object lockObj = new object();

public void MyMethod(string a) {

    if (list.Contains(a))
        return;

    lock (lockObj) {
        list.Add(a,"someothervalue");
    }
}

假设我MyMethod("mystring")同时从不同的线程调用。

是否有可能有多个线程(我们将其视为两个)(!list.Contains(a))同时进入 if 语句(有几个 CPU 周期差异),两个线程都被评估为false,一个线程进入临界区,而另一个被锁定在外面,所以第二个线程进入并"mystring"在第一个线程退出后再次添加到列表中,导致字典尝试添加重复键?

4

7 回答 7

18

不,它不是线程安全的。您也需要锁定,list.Contains因为在 if 测试和添加数据之间可能会切换线程并再次返回。另一个线程可能同时添加了数据。

于 2013-10-24T13:55:09.007 回答
11

您需要锁定整个操作(检查和添加),否则多个线程可能会尝试添加相同的值。

线程时间线

我建议使用,ConcurrentDictionary(TKey, TValue)因为它被设计为线程安全的。

private readonly ConcurrentDictionary<string, string> _items
    = new ConcurrentDictionary<string, string>();

public void MyMethod(string item, string value)
{
    _items.AddOrUpdate(item, value, (i, v) => value);
}
于 2013-10-24T14:05:42.240 回答
1

您需要锁定整个语句。您可能会在该部分遇到问题.Contains(您的代码现在的方式)

于 2013-10-24T13:55:37.273 回答
1

您应该在锁定后检查列表。例如

if (list.Contains(a))
return;

    lock (lockObj) {
       if (list.Contains(a))
         return;
       list.Add(a);
    }
}
于 2013-10-24T13:55:51.930 回答
1
private Dictionary<string, string> list = new Dictionary<string, string>();

public void MyMethod(string a) {
   lock (list) {
      if (list.Contains(a))
        return;
      list.Add(a,"someothervalue");
    }
}

看看这个锁定指南,很好

要记住的一些指导方针

  1. 通常在锁定多个可写值时锁定私有静态对象
  2. 不要锁定在类或本地方法之外的范围内的东西,例如lock(this),这可能导致死锁!
  3. 如果它是唯一同时访问的对象,您可以锁定正在更改的对象
  4. 确保您锁定的对象不为空!
  5. 您只能锁定引用类型
于 2013-10-24T14:01:01.660 回答
0

我将假设您的意思是 writeContainsKey而不是Contains. Containson aDictionary是显式实现的,因此无法通过您声明的类型访问它。1

您的代码不安全。原因是没有什么可以同时阻止ContainsKeyAdd执行。实际上,这会引入一些非常显着的失败场景。因为我查看了如何Dictionary实现,所以我可以看到您的代码可能会导致数据结构包含重复项的情况。我的意思是它实际上包含重复项。不一定会抛出异常。其他的失败场景越来越陌生,但我不会在这里讨论。

对您的代码的一项微不足道的修改可能涉及双重检查锁定模式的变体。

public void MyMethod(string a) 
{
  if (!dictionary.ContainsKey(a))
  {
    lock (dictionary)
    {
      if (!dictionary.ContainsKey(a))
      {
        dictionary.Add(a, "someothervalue");
      }
    }
  }
}

当然,由于我已经说过的原因,这并不安全。实际上,众所周知,除了最简单的情况(例如单例的规范实现),双重检查锁定模式很难在所有情况下都正确。这个主题有很多变化。您可以尝试使用TryGetValue默认索引器,但最终所有这些变体都是大错特错。

那么如何在没有锁定的情况下正确完成呢?你可以试试ConcurrentDictionary。它具有GetOrAdd在这些场景中真正有用的方法。你的代码看起来像这样。

public void MyMethod(string a) 
{
  // The variable 'dictionary' is a ConcurrentDictionary.
  dictionary.GetOrAdd(a, "someothervalue");
}

这就是它的全部。该GetOrAdd函数将检查该项目是否存在。如果没有,那么它将被添加。否则,它将不理会数据结构。这一切都以线程安全的方式完成。在大多数情况下,ConcurrentDictionary无需等待锁即可执行此操作。2


1顺便说一句,你的变量名也很讨厌。如果不是因为 Servy 的评论,我可能已经错过了我们谈论的是 aDictionary而不是 a的事实List。事实上,根据Contains通话,我首先认为我们在谈论一个List.

2读卡器完全无锁ConcurrentDictionary但是,编写者总是持有锁(即添加和更新;删除操作仍然是无锁的)。这包括GetOrAdd功能。不同之处在于数据结构维护了几个可能的锁定选项,因此在大多数情况下很少或没有锁定争用。这就是为什么这种数据结构被称为“低锁”或“并发”而不是“无锁”的原因。

于 2013-10-24T18:11:28.753 回答
-2

您可以先进行非锁定检查,但如果您想要线程安全,则需要在锁定内再次重复检查。这样你就不会锁定,除非你必须确保线程安全。

Dictionary<string, string> list = new Dictionary<string, string>();
object lockObj = new object();

public void MyMethod(string a) {

    if (list.Contains(a))
        return;

    lock (lockObj) {
       if (!list.Contains(a)){
        list.Add(a,"someothervalue");
       }
    }
}
于 2013-10-24T14:05:31.310 回答