如何在.NET中的多线程环境中管理延迟创建的对象的字典?

时间:2009-10-20 11:37:55

标签: .net multithreading singleton

这个问题是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模式的一个例子。

显然,访问已构建的对象时根本没有锁定惩罚。是否有可能设计一个懒惰创建的对象的集合,同样的精神是在特定对象已经创建时保持最小的惩罚?删除对象时可以支付更高的罚金。

感谢。

修改

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

10 个答案:

答案 0 :(得分:2)

您可以使用Lazy<T>个对象的字典,但您需要wait for .NET4borrow the code from Joe Duffy或自行编码。

您仍然需要处理如何同步对字典本身的访问权限的问题。我可能只是将字典访问权限包装在lock块中,然后再分析,以确定是否需要进一步优化。基于Monitor的锁实际上在没有太多争用的情况下具有相当好的性能。)

答案 1 :(得分:2)

正是在这种情况下,我仍然偶尔使用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 :(得分:1)

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

我假设如果你使用单身人士,他们的人数有限,他们最终会被需要,因此在预先创建它们时应该没有什么害处,因此避免锁定整个每次访问单身人士时都需要收集。

答案 3 :(得分:1)

  • 您可以拥有一个非惰性代理对象的非惰性集合;每个代理对象都引用该槽的“真实”内容,并且该引用可以在没有锁定的情况下进行空检查。

  • 如果您提前知道了一组延迟创建的对象,请完全忘记字典,只需为每个惰性对象创建一个带有专用字段的类;这些字段可以在没有锁定的情况下进行空检查。

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

答案 4 :(得分:0)

我看不出为什么在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];
}

答案 5 :(得分:0)

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

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

安全的安全方式是:

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()将导致在内存屏障写入屏障之前必须执行内存屏障之前的所有内存写入。

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

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

答案 6 :(得分:0)

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

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

答案 7 :(得分:0)

我只能想到这个......这种方法的一个问题是你必须事先知道字典的最大容量,因为它在内部会使用一个数组来比较和交换它。 / p>

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];
   }

}

当然你必须使用spinlock来检查密钥的存在,但是 应该让你没有争议。可能代码中缺少一些安全检查,以及FindEmptySpace的方法体,它在数组中找到一个空闲索引。

Joe Duffy在此articlethis other one中只有一个螺旋锁实现。 Spinlock也包含在Parallels Extensions和新的.Net 4.0

答案 8 :(得分:0)

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

答案 9 :(得分:0)

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

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