【问题标题】:How to manage a dictionary of lazily created objects in a multithreaded environment in .NET?如何在 .NET 的多线程环境中管理延迟创建的对象字典?
【发布时间】:2009-10-20 11:37:55
【问题描述】:

这个问题是this one 的延续。交易很简单。

鉴于:

  • 懒惰创建的单例集合。
  • 多线程环境。

通缉:

  • 当访问已创建的对象时,尽可能减少锁定损失的实现。对新对象的删除或延迟初始化支付更高的惩罚是可以的。

让我们考虑一下单例的老式 C++ 风格实现,用作 if-lock-if 模式的说明:

public static SomeType Instance
{
  get
  {
    if (m_instance == null)
    {
      lock(m_lock)
      {
        if (m_instance == null)
        {
          m_instance = new SomeType();
        }
      }
    }
    return m_instance;
  }
}

同样,这只是 if-lock-if 模式的一个说明。

显然,在访问已构造的对象时根本没有锁损失。当特定对象已经创建时,是否可以本着保持惩罚最小的相同精神设计一组延迟创建的对象?移走物件时罚款较高。

谢谢。

编辑:

我已经重写了这个问题,完全删除了单例这个词,因为人们往往过于关注它而不是问题的核心。

【问题讨论】:

  • 我不认为单例在这里是正确的名字,如果他们都是SomeType类型的话
  • SomeType 可以是接口,也可以是基本类型。真的没关系。
  • 单例设计模式最近已经失宠,因为它相当于全局状态(尤其是可变状态时)并降低了可测试性。您是否考虑过其他选项,例如 IoC 容器框架提供的生命周期管理?
  • 感谢您的建议。我已经更新了我的问题。
  • 这种模式称为“双重检查锁定”,而不是“if-lock-if”。它在一般情况下也会被破坏(例如,在 Java 中,它只有在 m_instance 是 volatile 时才会起作用)。

标签: .net multithreading singleton


【解决方案1】:

正是在这种情况下,我仍然偶尔使用 Hashtable。 Hashtable 是多读取器、单写入器线程安全的,因此您可以在双重检查锁定模式中使用它,就像使用字段一样:

public static SomeType Instance
{
  get
  {
    if (!m_ht.Contains(typeof(SomeType)))
    {
      lock(m_lock)
      {
        if (!m_ht.Contains(typeof(SomeType)))
        {
          m_ht[typeof(SomeType)] = new SomeType();
        }
      }
    }
    return (SomeType)m_ht[typeof(SomeType)];
  }
}

永远不要枚举所有的键或值。

【讨论】:

    【解决方案2】:

    您可以使用包含Lazy<T> 对象的字典,但您需要wait for .NET4、borrow the code from Joe Duffy 或自己编写代码。

    您仍然需要处理如何同步访问字典本身的问题。我可能只是将字典访问包装在 lock 块中并稍后配置文件以决定是否需要进一步优化。 (在没有太多争用的情况下,基于Monitor 的锁实际上具有相当不错的性能。)

    【讨论】:

    • 今天就是这样。该问题具有教育目的。
    • 您能否提供更具体的 Joe Duffy 代码链接?给定的链接很有趣,但我看不到相关性。谢谢。
    【解决方案3】:

    一个想法是在程序初始化期间创建字典,并立即用所需的单例/对象填充它。之后不要修改字典,只需以只读方式访问它,我假设字典对于阅读是线程安全的(如果不是这种情况,我会觉得很奇怪)。

    我假设如果您使用单例,它们的数量将是有限的,并且最终将需要它们,因此在前面创建它们应该没有什么坏处,因此避免了对整体的锁定每次需要访问单例时收集。

    【讨论】:

    • 感谢您的回复。我已经编辑了这个问题以澄清它。
    【解决方案4】:
    • 你可以有一个非惰性的惰性代理对象集合;每个代理对象都有一个对该插槽“真实”内容的引用,并且该引用可以在没有锁的情况下进行空值检查。

    • 1234563这些字段可以在没有锁的情况下进行空检查。
    • 不要使用字典,而是使用整数值作为键并将值存储在一个大数组中;数组中的引用可以在没有锁的情况下进行空值检查。

    【讨论】:

    • 很遗憾,我事先不知道物品的数量或钥匙。想想缓存——你提前知道未来要缓存的对象的键吗?
    • 是这是一个缓存吗?当您不断更改对问题的描述时,很难回答这个问题;单例库与缓存是一个非常不同的问题。
    • @Skirwan:这是一个非常重要的约束,需要添加到问题中。
    【解决方案5】:

    我不明白为什么 IDictionary<string, object> singletons 不能在 GetInstance() 中使用相同的双重检查方法:

    public object GetInstance(string key)
    {
          if(!singletons.ContainsKey(key))
               lock(syncRoot)
                    if(!singletons.ContainsKey)
                         singletons[key] = new ...();
    
          return singletons[key];
    }
    

    【讨论】:

    • 我很确定ContainsKey 方法不是线程安全的,所以您现在还有一些问题:如何同步访问singletons 字典,以及如何最小化同步的成本。
    • 安东,看看链接的问题。
    【解决方案6】:

    您的代码中的 Itanium 处理器存在潜在的多线程问题(除此之外,还有非常常见的初始化模式)。

    安腾处理器将分配变量的内存并在调用构造函数之前设置变量。因此,如果同时执行两个线程,则第二个线程会将实例设置为非空,即使它尚未初始化。

    安腾安全方式是这样的:

    public static SomeType Instance
    {
      get
      {
        if (m_instance == null)
        {
          lock(m_lock)
          {
            if (m_instance == null)
            {
              var tmpInstance = new SomeType();
              System.Threading.Thread.MemoryBarrier();
              m_Instance = tmpInstance;
            }
          }
        }
        return m_instance;
      }
    }
    

    MemoryBarrier() 将导致在内存屏障之前的所有内存写入都必须在屏障之后的内存写入之前执行。

    但除此之外,我认为它可能是静态成员最有效的惰性初始化(因为如果你想要急切的实例化,你可以在静态构造函数中完成)。

    还有其他方面,比如耦合。对声明为静态公共属性的单例的依赖会在您的系统中产生紧密耦合,并使其难以测试,但这是另一回事。

    【讨论】:

    • 谢谢。有趣,但问题不是关于单例,而是关于单例的集合。代码只是我想要实现的目标的说明。
    【解决方案7】:

    将您想要充当单例的所有类型注册到一个 windsor 容器实例中,并将生活方式设置为单例,并让容器为您管理它(没有单例的单例是最好的单例)。

    通过使用键注册每种类型,您可以使用 container.Resolve(key) 通过名称而不是类型来检索您需要的组件。

    【讨论】:

      【解决方案8】:

      我只能想到这个......这种方法的一个问题是您必须提前知道字典的最大容量,因为在内部它将使用一个数组来进行比较和交换。

      public class MyClass<TKey, TValue> where TValue : class, new
      {
         private bool lockTaken = false;
         private SpinLock mSpinLock = new SpinLock();
      
         private readonly TValue[] myObjects;
         private readonly LinkedList<int> freeSpaces = new LinkedList<int>();
      
         private Dictionary<TKey, int> map = new Dictionary<TKey, int>();        
      
      
         private TValue Lazy(int ix)
         {
            // Atomically create the object if needed
            Interlocked.CompareExchange<TValue>(ref myObjects[ix], new TValue(), null);
            return (myObjects[ix]);
         }
      
         public TValue LazyGetValue(TKey key)
         {
            try
            {
               // Check for key existance or add it
               mSpinLock.Enter(ref lockTaken);
               if (map.ContainsKey(key))
                  int ix = map[key];
               else // Find an empty space in the array
                  map[key] = FindEmptySpace();                                 
            }
            finally
            {
               if (lockTaken)
               {
                  mSpinLock.Exit();
                  // Lazy create the object if needed
                  if (myObjects[ix] != null)
                     return myObjects[ix];
                  else            
                     return Lazy(ix);
               }
            }            
            throw new Exception("Couldn't lock"); 
         }
      
         public MyClass(int maxCapacity)
         {
            myObjects = new TValue[maxCapacity];
         }
      

      }

      当然,您必须自旋锁以检查密钥的存在,但 应该 让您很少争用。代码中可能缺少一些安全检查,以及 FindEmptySpace 的方法体,它在数组中找到一个空闲索引。

      Joe Duffy 在 article 和 this other one 中实现了一个自旋锁。 Spinlock 也包含在 Parallels Extensions 和新的 .Net 4.0 中

      【讨论】:

      • 感谢您的回复。我没有深入检查您的代码,但 SpinLock.Enter 比简单的 Monitor.Enter 便宜,如果我没记错的话,它对应于关键部分?
      • Spinlock 可以更便宜,请参阅更新的答案以获取链接。基本上,您在锁定之前忙着等待片刻,因此对于多核场景(真正的多线程)中的快速进出线程,您可以获得比标准锁真正的改进。
      【解决方案9】:

      我建议您编写自己的字典实现(或提供自己的多线程包装器)。 .net 字典(和多线程的包装器)有一个相当简单的锁模式。您需要的是在一个非常细粒度的级别上控制锁,例如:检索一个对象不需要修改内部数据结构,因此允许多个检索同时发生是可以的。此回复无法涵盖完整的竞争控制策略,但我建议您通过阅读 SQL Server 如何使用读锁、写锁、更新锁来走捷径……您可以在 MSDN 中找到这些信息。

      【讨论】:

        【解决方案10】:

        我能想到的最简单的解决方案是包装 Hashtable 以获得类型和线程安全。您可以这样做,因为HashTable 类对于多个读取器和单个写入器是线程安全的。缺点是 - 据我所知 - Hashtable 比 Dictionary&lt;TKey, TValue&gt; 慢。对于像我这样关心细节的开发者来说,使用Objects 的集合需要付出相当大的努力。

        internal abstract class LazyInitializedDictionary<TKey, TValue>
        {
           private readonly Hashtable store = new Hashtable();
        
           internal TValue this[TKey key]
           {
              get
              {
                 if (!this.store.ContainsKey(key))
                 {
                    lock (this.store)
                    {
                       if (!this.store.ContainsKey(key))
                       {
                          this.store.Add(key, this.CreateNewValue(key));
                       }
                    }
                 }
        
                 return (TValue)this.store[key];
              }
           }
        
           internal Boolean Remove(TKey key)
           {
              if (this.store.ContainsKey(key))
              {
                 lock (this.store)
                 {
                    if (this.store.ContainsKey(key))
                    {
                       this.store.Remove(key);
        
                       return true;
                    }
                 }
              }
        
              return false;
           }   
        
           protected abstract TValue CreateNewValue(TKey key);
        }
        

        有了这个,你可以创建一个派生类来实现你想要的行为。例如一个非常有帮助的类使String.Length过时了...;)

        internal sealed class StringLengthLookup :
           LazyInitializedDictionary<String, Int32>
        {
           protected override Int32 CreateNewValue(String key)
           {
              return key.Length;
           }
        }
        

        【讨论】:

          猜你喜欢
          • 2010-12-08
          • 1970-01-01
          • 2014-01-16
          • 2022-08-22
          • 2010-11-17
          • 2012-02-17
          • 1970-01-01
          • 2013-01-31
          • 1970-01-01
          相关资源
          最近更新 更多