锁定一个实习的字符串?

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

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

这是我的代码的简化版本:

 public static T CheckCache(string key, Func 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; } 

另外,假设我在编译时不知道key所有可能值。

如何使这个线程安全? 我知道我需要在这里引入锁定,以防止1+线程将我的条件评估为真,但我不知道要锁定什么。 我读过的许多关于锁定的例子(例如Jon Skeet的文章 )建议使用仅用于锁定的“虚拟”私有变量。 在这种情况下,这是不可能的,因为在编译时密钥是未知的。 我知道我可以通过为每个key使用相同的锁来简单地使这个线程安全,但这可能是浪费。

现在,我的主要问题是:

是否可以锁定key 字符串实习会有帮助吗?

在读完内部的.NET 2.0字符串后 ,我明白我可以显式调用String.Intern()来获取从字符串的值到字符串实例的1到1映射。 这适合锁定吗? 我们将上面的代码更改为:

 public static T CheckCache(string key, Func fn, DateTime expires) { //check for the scenario where two strings with the same value are stored at different memory locations key = String.Intern(key); lock (key) //is this object suitable for locking? { 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; } } 

以上实现线程安全吗?

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

  1. 代码库的其他部分可能会出于不同的目的锁定在相同的实习字符串实例上 ,如果幸运的话只会导致性能问题,如果不幸则会导致死锁(可能仅在将来,随着代码库的增长,编码器会在不知道您的情况下进行扩展) String.Intern locking pattern) – 请注意,这包括对同一个interned字符串的锁定, 即使它们位于不同的AppDomain中 ,可能会导致跨AppDomain死锁
  2. 如果你决定这样做,你就不可能收回实习记忆
  3. String.Intern()很慢

要解决所有这三个问题,您可以实现自己的Intern() ,它与您的特定锁定目的相关联 ,即不要将其用作全局通用字符串interner

 private static readonly ConcurrentDictionary concSafe = new ConcurrentDictionary(); static string InternConcurrentSafe(string s) { return concSafe.GetOrAdd(s, String.Copy); } 

我调用了这个方法...Safe() ,因为在实习时我不会存储传入的String实例,因为它可能是一个已经实例化的String ,使其受到上面1.中提到的问题的影响。

为了比较各种实习字符串方式的性能,我还尝试了以下两种方法,以及String.Intern

 private static readonly ConcurrentDictionary conc = new ConcurrentDictionary(); static string InternConcurrent(string s) { return conc.GetOrAdd(s, s); } private static readonly Dictionary locked = new Dictionary(5000); static string InternLocked(string s) { string interned; lock (locked) if (!locked.TryGetValue(s, out interned)) interned = locked[s] = s; return interned; } 

基准

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

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

结果

  • String.Intern ():2032 ms
  • InternLocked() :1245毫秒
  • InternConcurrent() :458 ms
  • InternConcurrentSafe() :453 ms

InternConcurrentSafeInternConcurrent一样快的事实是有道理的,因为这些数字在预热之后(参见上面的NB),因此在测试期间实际上没有或只有少量的String.Copy调用。


为了正确封装它,创建一个这样的类:

 public class StringLocker { private readonly ConcurrentDictionary _locks = new ConcurrentDictionary(); public string GetLockObject(string s) { return _locks.GetOrAdd(s, String.Copy); } } 

并且在为每个用例实例化一个StringLocker之后,它就像调用一样简单

 lock(myStringLocker.GetLockObject(s)) { ... 

NB

再想一想,如果你想要做的只是锁定它就没有必要返回一个string类型的对象 ,所以复制字符是完全没必要的,并且以下将比上面的类表现更好。

 public class StringLocker { private readonly ConcurrentDictionary _locks = new ConcurrentDictionary(); public object GetLockObject(string s) { return _locks.GetOrAdd(s, k => new object()); } } 

丹尼尔答案的一个变种……

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

 public static T CheckCache(string key, Func 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(); 

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

我会采用务实的方法并使用虚拟变量。
如果由于某种原因这是不可能的,我会使用一个Dictionary其中key作为键,一个虚拟对象作为值并锁定该值,因为字符串不适合锁定:

 private object _syncRoot = new Object(); private Dictionary _syncRoots = new Dictionary(); public static T CheckCache(string key, Func 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; } } 

但是,在大多数情况下,这是过度杀伤和不必要的微观优化。

永远不要锁定弦乐。 尤其是那些被拘禁的人。 有关锁定实习字符串的危险,请参阅此博客文章 。

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

 object myLock = new object(); 

根据文档 ,Cache类型是线程安全的。 因此,不同步自己的缺点是,在创建项目时,可能会在其他线程意识到不需要创建项目之前创建几次。

如果情况只是缓存常见的静态/只读事物,那么不要只是为了保存可能发生的奇怪的几次冲突而烦扰同步。 (假设碰撞是良性的。)

锁定对象不是特定于字符串的,它将特定于您需要的锁的粒度。 在这种情况下,您尝试锁定对缓存的访问,因此一个对象将服务锁定缓存。 锁定特定键的想法并不是锁定通常所关注的概念。

如果你想多次停止昂贵的调用,那么你可以将加载逻辑分解为一个新的类LoadMillionsOfRecords ,根据Oded的答案调用.Load并锁定内部锁定对象一次。

我在@ eugene-beresovsky 回答的启发下添加了Bardock.Utils包中的解决方案。

用法:

 private static LockeableObjectFactory _lockeableStringFactory = new LockeableObjectFactory(); string key = ...; lock (_lockeableStringFactory.Get(key)) { ... } 

解决方案代码

 namespace Bardock.Utils.Sync { ///  /// 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 ///  /// Type of the object you want lock on public class LockeableObjectFactory { private readonly ConcurrentDictionary _lockeableObjects = new ConcurrentDictionary(); ///  /// Creates or uses an existing object instance by specified seed ///  ///  /// The object used to generate a new lockeable object. /// The default EqualityComparer is used to determine if two seeds are equal. /// The same object instance is returned for equal seeds, otherwise a new object is created. ///  public object Get(TSeed seed) { return _lockeableObjects.GetOrAdd(seed, valueFactory: x => new object()); } } }