【问题标题】:Is it more efficient to lock or try-catch dictionary in a thread (.NET)在线程(.NET)中锁定或尝试捕获字典是否更有效
【发布时间】:2011-10-19 13:56:57
【问题描述】:

我有一个通用字典,用作线程 .NET 项目 (C#) 中的缓存。我将在字典上进行大量阅读(在高峰时间可能每秒数百次或更多)。

我有一个简单的 Get(int id) 方法,如果它在缓存中,它应该返回一个 CustomObject,否则返回 null。

我的问题: 锁定字典是更快/更有效,还是只使用 try/catch 块?

假设字典存储在名为“dic”的变量中。

锁定样本:

public CustomObject Get(int id)
{
   lock(dic)
   {
      if (dic.ContainsKey(id))
         return dic[id];
   }
   return null;
}

尝试/捕获样本:

public CustomObject Get(int id)
{
   try
   {
      return dic[id];
   }
   catch
   {
      return null;
   }
}

【问题讨论】:

    标签: .net multithreading dictionary locking try-catch


    【解决方案1】:

    我认为您应该在自己的环境中对其进行测试。基本上:

    • 锁很便宜
    • 尝试不出现异常很便宜,甚至可能比锁定更便宜
    • 尝试获取异常是非常昂贵的

    所以现在的问题是,您希望多久发生一次缓存未命中,从而引发异常。我会选择 lock() 因为它的执行时间不取决于你是否会获得缓存命中,这意味着它更可预测和可测量,同时仍然 - 非常便宜。我认为每秒数百次点击不会有任何问题。

    我所做的简单测试表明,使用 try/catch 获取缓存未命中非常非常昂贵。

    编辑

    简单测试表明:

    • 100k 次检索的 try-no throw 成本约为 2 毫秒
    • 锁定花费大约 6 毫秒
    • try-throw 大约需要 4 秒

    这意味着,获得了 lock(),因为如果您在每数千次尝试中获得超过 1 次缓存未命中,它比 try/catch 更有效,而且它更加稳定,不依赖于运气。

    【讨论】:

    • 在这种情况下测量 try/catch 是没有意义的,因为它不是线程安全的。请参阅下面的 Brian Gideon 回复。
    • 系统锁并不便宜!这完全取决于争用程度、内核数量、其他应用程序加载的 CPU 量等。100k 完成在 6 毫秒内 ?这看起来更像是对锁定部分的单线程访问,大约 60 ns,大约 50 ns 需要无竞争的 lock(),其余的是字典访问。您的基准测试似乎是在非竞争 lock() 上完成的,在这种情况下,无需测量锁。
    【解决方案2】:

    您可以继续注销 try-catch 选项。我不知道它是否更慢,但我知道如果有另一个线程更新Dictionary,它不会总是产生正确、一致和可预测的结果。问题是,在某些时候,作者会让Dictionary 处于半生不熟的状态,并且不知道读者会看到什么。这是行不通的。

    选项 1:如果您可以使用 .NET 4.0,那么我会使用 ConcurrentDictionary

    选项 2:如果您使用的是 .NET 3.5,那么您可以下载 Reactive Extensions 反向端口。 ConcurrentDictionary 包含在 System.Threading.dll 中。

    选项 3: 另一个想法是保留 Dictionary 的两个单独副本。一个仅用于阅读,另一个将作为接受更新的官方副本。每当您更新“官方”Dictionary 时,您都会克隆它并覆盖副本的引用。

    public class Example
    {
      // This is the official version which can accept updates.
      private readonly Dictionary<int, CustomObject> official = new Dictionary<int, CustomObject>();
    
      // This is a readonly copy. This must be marked as volatile for this to work correctly.
      private volatile Dictionary<int, CustomObject> copy = new Dictionary<int, CustomObject>();
    
      public class Example()
      {
      }
    
      public void Set(int id, CustomObject value)
      {
        lock (official)
        {
          // Update the official dictionary.
          official[id] = value;
    
          // Now create a clone of the official dictionary.
          var clone = new Dictionary<int, CustomObject>();
          foreach (var kvp in official)
          {
            clone.Add(kvp.Key, kvp.Value);
          }
    
          // Swap out the reference. 
          copy = clone;
        }
      }
    
      public CustomObject Get(int id)
      {   
        // No lock is required here.
        CustomObject value = null;
        if (copy.TryGetValue(id, out value))
        {
          return value;
        }
        return null;
      }
    
    }
    

    如果Dictionary中有很多项目,或者如果对官方副本的更新频繁发生,则此选项不起作用。但是,这是我不时使用的技巧。

    选项 4:同样合理的方法是坚持使用普通的旧 lock

    【讨论】:

    • @Dmitry:我拒绝了您将ReaderWriterLockSlim 作为选项#5 的编辑。原因是它比普通的旧 lock 慢 2 倍左右。 RWLS 在其保护的操作较长且抽出时效果更好。否则,没有足够的工作来克服开销。读取和更新Dictionary 是相对较快的操作,因此普通的lock 可能会更快,并且肯定会产生更紧凑的代码。不过这是个好主意:)
    • 锁获取本身可能比简单锁慢,但它允许更好的并发性和吞吐量,因为读者不会互相阻塞。在简单锁的情况下,线程在读取时会互相等待。这一点尤其重要,因为问题提到“在字典上进行大量阅读”。当然必须进行测量,但我认为 ReaderWriterLockSlim 是一个可行的选择。
    • @Dmitry:别误会我的意思。 RWLS 实际上是一个非常好的主意。今天早上我做了一个快速测试,我只是无法让 RWLS slim 变体比普通的旧 lock 更快地工作。我在单核机和双核机上试了一下。 lock 最终明显更快。同样,这是因为在关键部分执行的工作不足以克服 RWLS 开销。请注意,在测试时,我的读写器比率为 100 比 1。我想如果我有 8 核或 16 核机器,我可能会开始看到改进……不确定。
    • 构造函数有问题,copy=official。副本必须是自己的空字典实例。 Set 可以与 Get 和 Set 在 official[id] = value 中同时调用;但在第一次更新之前,官方==复制。所以它没有很好地处理以线程安全方式初始化的对象。
    • @ipavlu:不错的收获!我修好了。
    【解决方案3】:

    在你的场景中,我会选择lock/interlocked。如果您不锁定,您可能会收到损坏/无效的数据,并尝试在其他地方向字典中写入内容。

    如果你太在意性能,你可以使用Interlocked类...有很多技术可以使用它,以实现比lock更高性能的锁定。

    使用 Interlocked.CompareExchange 和控制变量可以实现。

    联锁示例:

    我在 Microsoft 网站上找到了这个示例(只是复制以便您可以在此处查看):

    来源:http://msdn.microsoft.com/en-us/library/system.threading.interlocked(v=VS.100).aspx#Y1291

            if(0 == Interlocked.Exchange(ref usingResource, 1))
            {
                Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name);
    
                //Code to access a resource that is not thread safe would go here.
    
                //Simulate some work
                Thread.Sleep(500);
    
                Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name);
    
                //Release the lock
                Interlocked.Exchange(ref usingResource, 0);
                return true;
            }
            else
            {
                Console.WriteLine("   {0} was denied the lock", Thread.CurrentThread.Name);
                return false;
            }
    

    请注意,在此示例中,if 块内的几乎所有内容都已锁定!

    变量usingResource是类的一个字段,通过引用传递给方法……就是我前面提到的控制变量。

    【讨论】:

      【解决方案4】:

      使用锁总是更安全、更干净,但是性能会因争用锁的线程数量而异

      【讨论】:

        【解决方案5】:

        锁定和争用 => 不便宜!!

        拜托,系统锁并不便宜,它必须在处理器的环形级别之间切换,因为锁是由操作系统提供的。为了这个荣誉,如果锁没有被争用,lock() 至少需要 50ns!如果有争用,情况会变得非常糟糕。

        使用 lock() 进行 6ms/100k 检索等基准测试与使用非竞争 lock() 进行的基准测试更加一致,这使得每次访问锁定检索的时间为 60ns。在多核和受同步原语保护的并发数据结构上,必须创建来自多个线程的争用,只有这样我们才能获得有关其行为的信息。

        当同时有更多线程访问锁时,它会变得非常快非常不高兴,因为等待的线程被取消调度/重新调度,并且要休眠的线程受线程时间片的影响,然后争用迫使它快速从几十或几百纳秒到几十毫秒。

        如果您想要高吞吐量、通过数据结构在多核 CPU 上的多个线程之间推送数据或线程之间的高响应性,基本上锁定是万恶之源。

        为此,您必须另辟蹊径。基于自旋锁的并发数据结构,重新设计线程交互方式,使交互松耦合,因此交互花费最少的时间和最少的项目。

        这可能是来自 TPL 的最佳 ConcurrentDictionary,在 dotNET 3.5(在 nuget 上查找 TPL)和更新版本中。

        【讨论】:

          【解决方案6】:

          Brien Gideon 回答中的改进类示例。当多次阅读超过写作时,这是一个好主意。我把它做成了泛型类,用互锁方法替换了 volatile,并在第一次初始化期间修复了一个可能的竞争条件。

          using System.Threading;
          using System.Collections.Generic;
          
          
          public class Example<TKeys, TValues>
          {
              private readonly object obj = new object();
          
              // Read/Write version.
              private readonly Dictionary<TKeys, TValues> official = new Dictionary<TKeys, TValues>();
          
              // Read/Replace version. 
              //it must be initialized as new empty dictionary as well,
              // they can not share reference or it won't be thread safe
              //initially because Set be inserting data into official and copy
              // in the same time.
              private Dictionary<TKeys, TValues> copy = new Dictionary<TKeys, TValues>();
          
              public void Set(TKeys id, TValues value)
              {
                  lock (obj)
                  {
                      // Update the official dictionary.
                      official[id] = value;
          
                      // Now create a clone of official dictionary
                      var clone = new Dictionary<int, CustomObject>(official);
          
                      // interlocked sets atomically latest reference
                      Interlocked.Exchange(ref copy, clone);
                  }
              }
          
              public bool TryGetValue(TKeys id, out TValues value)
              {
                  // interlocked gets latest reference atomically,
                  //also providing access to full TryGetValue makes it
                  //safer for struct data types, if dictionary empty,
                  //it wont return default value!
                  return
                  Interlocked
                  .CompareExchange(ref copy, null, null)
                  .TryGetValue(id, ref value)
                  ;
              }
          
          }
          

          【讨论】:

            猜你喜欢
            • 2023-04-07
            • 2012-06-26
            • 1970-01-01
            • 2023-03-12
            • 2016-04-10
            • 1970-01-01
            • 2013-08-30
            • 2020-05-15
            • 1970-01-01
            相关资源
            最近更新 更多