C# 锁定一根插入的绳子?

C# 锁定一根插入的绳子?,c#,locking,thread-safety,higher-order-functions,C#,Locking,Thread Safety,Higher Order Functions,更新:如果此方法不是线程安全的,则可以接受,但我有兴趣了解如何使其成为线程安全的。另外,如果可以避免的话,我不想为key的所有值锁定单个对象 原始问题:假设我想编写一个高阶函数,该函数接受一个键和一个函数,并检查是否使用给定键缓存了一个对象。如果有,则返回缓存的值。否则,将运行给定函数并缓存并返回结果 以下是我的代码的简化版本: public static T CheckCache<T>(string key, Func<T> fn, DateTime expires)

更新:如果此方法不是线程安全的,则可以接受,但我有兴趣了解如何使其成为线程安全的。另外,如果可以避免的话,我不想为
key
的所有值锁定单个对象

原始问题:假设我想编写一个高阶函数,该函数接受一个键和一个函数,并检查是否使用给定键缓存了一个对象。如果有,则返回缓存的值。否则,将运行给定函数并缓存并返回结果

以下是我的代码的简化版本:

public static T CheckCache<T>(string key, Func<T> fn, DateTime expires)
{
    object cache = HttpContext.Current.Cache.Get(key);
    //clearly not thread safe, two threads could both evaluate the below condition as true
    //what can I lock on since the value of "key" may not be known at compile time?
    if (cache == null)
    {
        T result = fn();
        HttpContext.Current.Cache.Insert(key, result, null, expires, Cache.NoSlidingExpiration);
        return result;
    }
    else
        return (T)cache;
}

上述实现是线程安全的吗?

永远不要锁定字符串。特别是那些被拘留的人。请参阅关于锁定插入管柱的危险的说明

只需创建一个新对象并锁定:

object myLock = new object();
,缓存类型是线程安全的。因此,不同步自己的缺点是,当创建项时,可能会在其他线程意识到不需要创建它之前创建几次

如果情况只是缓存常见的静态/只读内容,那么不要为了保存可能发生的奇数冲突而费心进行同步。(假设碰撞是良性的。)

锁定对象不会特定于字符串,而是特定于所需锁的粒度。在本例中,您试图锁定对缓存的访问,因此一个对象将服务于锁定缓存。锁定进入的特定密钥的想法并不是锁定通常涉及的概念


如果您想阻止昂贵的调用多次发生,那么您可以将加载逻辑撕成一个新类
LoadMillionsOfRecords
,调用
。加载
,并根据Oded的回答在内部锁定对象上锁定一次。

我会采用实用的方法,并使用虚拟变量。
如果出于任何原因,这是不可能的,我会使用
字典
,其中
作为键,虚拟对象作为值并锁定该值,因为字符串不适合锁定:

private object _syncRoot = new Object();
private Dictionary<string, object> _syncRoots = new Dictionary<string, object>();

public static T CheckCache<T>(string key, Func<T> fn, DateTime expires)
{
    object keySyncRoot;
    lock(_syncRoot)
    {

        if(!_syncRoots.TryGetValue(key, out keySyncRoot))
        {
            keySyncRoot = new object();
            _syncRoots[key] = keySyncRoot;
        }
    }
    lock(keySyncRoot)
    {

        object cache = HttpContext.Current.Cache.Get(key);
        if (cache == null)
        {
            T result = fn();
            HttpContext.Current.Cache.Insert(key, result, null, expires, 
                                             Cache.NoSlidingExpiration);
            return result;
        }
        else
            return (T)cache;
    }
}
private object\u syncRoot=new object();
专用词典_syncroot=新词典();
公共静态T CheckCache(字符串键,Func fn,DateTime expires)
{
对象键同步器;
锁定(\u syncRoot)
{
if(!\u syncRoots.TryGetValue(键,out键同步键))
{
keySyncRoot=新对象();
_syncRoots[key]=keySyncRoot;
}
}
锁(键同步器)
{
objectcache=HttpContext.Current.cache.Get(key);
if(缓存==null)
{
T结果=fn();
HttpContext.Current.Cache.Insert(键、结果、null、过期、,
Cache.NoSlidingExpiration);
返回结果;
}
其他的
返回(T)缓存;
}
}
然而,在大多数情况下,这是过度的和不必要的微观优化

与为每个字符串创建一个新的锁对象不同,您可以共享一小组锁,根据字符串的哈希代码选择要使用的锁。这意味着,如果您可能拥有数千或数百万个密钥,那么GC压力将更小,并且应该允许足够的粒度来避免任何严重的阻塞(如果必要,可能需要进行一些调整)

publicstatict检查缓存(字符串键,Func-fn,DateTime过期)
{
objectcached=HttpContext.Current.Cache[key];
如果(缓存!=null)
返回(T)缓存;
int stripeIndex=(key.GetHashCode()&0x7FFFFFFF)%\u stripes.Length;
锁定(_stripes[stripeIndex])
{
T结果=fn();
HttpContext.Current.Cache.Insert(键、结果、null、过期、,
Cache.NoSlidingExpiration);
返回结果;
}
}
//共享一组32个锁
私有静态只读对象[]_stripes=Enumerable.Range(0,32)
.Select(x=>newobject())
.ToArray();

这将允许您通过更改
\u stripes
数组中的元素数量来调整锁定粒度,以满足您的特殊需要。(但是,如果您需要接近每个字符串粒度一个锁,那么最好使用Daniel的答案。)

前面部分提到的@wsanville自己的解决方案的问题:

  • 代码库的其他部分可能出于不同的目的锁定相同的内部字符串实例,如果运气好,则只会导致性能问题,如果运气不好,则会导致死锁(可能仅在将来,随着代码库的增长,程序员会在不知道您的
    string.Intern
    锁定模式的情况下进行扩展)-注意,这包括同一个插入字符串上的锁,可能导致跨AppDomain死锁
  • 如果您决定这样做,您不可能回收内存
  • String.Intern()
    速度慢
  • 要解决所有这三个问题,您可以实现自己的
    Intern()
    ,并将其与特定的锁定目的联系起来,即不要将其用作全局通用字符串interner:


    基准

    100个线程,每个线程随机选择5000个不同字符串中的一个(每个字符串包含8个数字)50000次,然后调用相应的intern方法。充分预热后的所有值。这是Windows7,64位,在4core i5上

    注意:预热上述设置意味着预热后,不会对相应的实习词典进行任何写入,而只进行读取。这是我对手头的用例感兴趣的,但是不同的写/读比率可能会影响结果

    结果

    • String.Intern
      ():2032毫秒
    • Inter
      
      private object _syncRoot = new Object();
      private Dictionary<string, object> _syncRoots = new Dictionary<string, object>();
      
      public static T CheckCache<T>(string key, Func<T> fn, DateTime expires)
      {
          object keySyncRoot;
          lock(_syncRoot)
          {
      
              if(!_syncRoots.TryGetValue(key, out keySyncRoot))
              {
                  keySyncRoot = new object();
                  _syncRoots[key] = keySyncRoot;
              }
          }
          lock(keySyncRoot)
          {
      
              object cache = HttpContext.Current.Cache.Get(key);
              if (cache == null)
              {
                  T result = fn();
                  HttpContext.Current.Cache.Insert(key, result, null, expires, 
                                                   Cache.NoSlidingExpiration);
                  return result;
              }
              else
                  return (T)cache;
          }
      }
      
      public static T CheckCache<T>(string key, Func<T> fn, DateTime expires)
      {
          object cached = HttpContext.Current.Cache[key];
          if (cached != null)
              return (T)cached;
      
          int stripeIndex = (key.GetHashCode() & 0x7FFFFFFF) % _stripes.Length;
      
          lock (_stripes[stripeIndex])
          {
              T result = fn();
              HttpContext.Current.Cache.Insert(key, result, null, expires,
                                               Cache.NoSlidingExpiration);
              return result;
          }
      }
      
      // share a set of 32 locks
      private static readonly object[] _stripes = Enumerable.Range(0, 32)
                                                            .Select(x => new object())
                                                            .ToArray();
      
      private static readonly ConcurrentDictionary<string, string> concSafe = 
          new ConcurrentDictionary<string, string>();
      static string InternConcurrentSafe(string s)
      {
          return concSafe.GetOrAdd(s, String.Copy);
      }
      
      private static readonly ConcurrentDictionary<string, string> conc = 
          new ConcurrentDictionary<string, string>();
      static string InternConcurrent(string s)
      {
          return conc.GetOrAdd(s, s);
      }
      
      private static readonly Dictionary<string, string> locked = 
          new Dictionary<string, string>(5000);
      static string InternLocked(string s)
      {
          string interned;
          lock (locked)
              if (!locked.TryGetValue(s, out interned))
                  interned = locked[s] = s;
          return interned;
      }
      
      public class StringLocker
      {
          private readonly ConcurrentDictionary<string, string> _locks =
              new ConcurrentDictionary<string, string>();
      
          public string GetLockObject(string s)
          {
              return _locks.GetOrAdd(s, String.Copy);
          }
      }
      
      lock(myStringLocker.GetLockObject(s))
      {
          ...
      
      public class StringLocker
      {
          private readonly ConcurrentDictionary<string, object> _locks =
              new ConcurrentDictionary<string, object>();
      
          public object GetLockObject(string s)
          {
              return _locks.GetOrAdd(s, k => new object());
          }
      }
      
      private static LockeableObjectFactory<string> _lockeableStringFactory = 
          new LockeableObjectFactory<string>();
      
      string key = ...;
      
      lock (_lockeableStringFactory.Get(key))
      {
          ...
      }
      
      namespace Bardock.Utils.Sync
      {
          /// <summary>
          /// Creates objects based on instances of TSeed that can be used to acquire an exclusive lock.
          /// Instanciate one factory for every use case you might have.
          /// Inspired by Eugene Beresovsky's solution: https://stackoverflow.com/a/19375402
          /// </summary>
          /// <typeparam name="TSeed">Type of the object you want lock on</typeparam>
          public class LockeableObjectFactory<TSeed>
          {
              private readonly ConcurrentDictionary<TSeed, object> _lockeableObjects = new ConcurrentDictionary<TSeed, object>();
      
              /// <summary>
              /// Creates or uses an existing object instance by specified seed
              /// </summary>
              /// <param name="seed">
              /// The object used to generate a new lockeable object.
              /// The default EqualityComparer<TSeed> is used to determine if two seeds are equal. 
              /// The same object instance is returned for equal seeds, otherwise a new object is created.
              /// </param>
              public object Get(TSeed seed)
              {
                  return _lockeableObjects.GetOrAdd(seed, valueFactory: x => new object());
              }
          }
      }