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上 注意:预热上述设置意味着预热后,不会对相应的实习词典进行任何写入,而只进行读取。这是我对手头的用例感兴趣的,但是不同的写/读比率可能会影响结果 结果
():2032毫秒String.Intern
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()); } } }