【问题标题】:C# Is this method thread safe?C#这个方法线程安全吗?
【发布时间】:2013-10-24 13:53:37
【问题描述】:

考虑以下代码:

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")

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

【问题讨论】:

  • 调用字典list 真的很卑鄙。它给人的印象是它是一个列表,而不是字典。
  • @Servy 我将我的狗命名为“猫”。这也意味着什么?
  • @Cruncher 不,因为猫太棒了。
  • @Servy 所以你的意思是你不喜欢列表?

标签: c# multithreading


【解决方案1】:

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

【讨论】:

    【解决方案2】:

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

    我建议使用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);
    }
    

    【讨论】:

    • 我相信你的图表是错误的。您需要在线程 1 添加项目后移动线程 2 的获取锁。
    • 我应该写成“尝试获取锁定”。 T1 将获取锁,T2 将被阻塞,直到 T1 离开锁。
    【解决方案3】:

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

    【讨论】:

      【解决方案4】:

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

      if (list.Contains(a))
      return;
      
          lock (lockObj) {
             if (list.Contains(a))
               return;
             list.Add(a);
          }
      }
      

      【讨论】:

      • 这个问题是微软不保证 list.Contains 方法在另一个线程正在修改集合时会起作用
      • 这似乎不是 100% 安全的。另一个线程可能正在写入列表,因为正在评估锁定区域中的 Contains()。
      【解决方案5】:
      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");
          }
      }
      

      看看这个locking的指南,很好

      需要牢记的几条准则

      1. 当锁定多个可写值时,通常锁定私有静态对象
      2. 不要锁定在类或本地方法之外的对象,例如lock(this),这可能会导致死锁!
      3. 如果正在更改的对象是唯一同时访问的对象,您可以锁定它
      4. 确保您锁定的对象不为空!
      5. 您只能锁定引用类型

      【讨论】:

        【解决方案6】:

        我假设您的意思是写ContainsKey 而不是ContainsDictionary 上的 Contains 已显式实现,因此无法通过您声明的类型访问它。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 的评论,我可能已经错过了我们谈论的是 Dictionary 而不是 List 的事实。事实上,基于Contains 电话,我首先认为我们在谈论List

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

        【讨论】:

        【解决方案7】:

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

        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");
               }
            }
        }
        

        【讨论】:

        • 这不安全。 ContainsAdd 仍然可以同时执行。
        • 是的,但这会导致任何不一致问题吗?
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-07-16
        • 2013-10-12
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多