更新:如果此方法不是线程安全的,可以接受,但我有兴趣了解如何使其保持线程安全。另外,如果可以避免,我不想为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;
}
另外,假设我在编译时不知道key
的所有可能值。
如何使这个线程安全?我知道我需要在这里引入锁定,以防止1+线程将我的条件评估为真,但我不知道要锁定什么。我读过的许多关于锁定的例子(例如Jon Skeet's article)建议使用仅用于锁定的“虚拟”私有变量。在这种情况下,这是不可能的,因为密钥在编译时是未知的。我知道我可以通过为每个key
使用相同的锁来轻松地使这个线程安全,但这可能是浪费。
现在,我的主要问题是:
是否可以锁定key
?字符串实习会有帮助吗?
在阅读.NET 2.0 string interning inside out之后,我明白我可以显式调用String.Intern()
来获取从字符串值到字符串实例的1对1映射。 这是否适合锁定?让我们将上面的代码更改为:
public static T CheckCache<T>(string key, Func<T> 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;
}
}
上述实现线程是否安全?
答案 0 :(得分:21)
@ wsanville自己的解决方案的问题,部分在前面提到过:
String.Intern
锁定模式) - 请注意,这包括锁定在同一个实际字符串even if they are in different AppDomains上,可能导致跨AppDomain死锁String.Intern()
很慢 要解决所有这三个问题,您可以实施自己的Intern()
,这与您的特定锁定目的相关,即不要将其用作全局通用 string interner :
private static readonly ConcurrentDictionary<string, string> concSafe =
new ConcurrentDictionary<string, string>();
static string InternConcurrentSafe(string s)
{
return concSafe.GetOrAdd(s, String.Copy);
}
我调用了这个方法...Safe()
,因为在实习时我不会存储传入的String
实例,因为这可能是是一个已经被拘禁的String
,使其受到上述1.中提到的问题的影响。
为了比较各种实习字符串方式的性能,我还尝试了以下两种方法,以及String.Intern
。
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;
}
<强>基准强>
100个线程,每个线程随机选择5000个不同字符串(每个包含8个数字)中的一个50000次,然后调用相应的实习方法。充分预热后的所有数值。这是4核i5上的Windows 7,64位。
N.B。预热上述设置意味着在预热后,不会有写入到相应的实习词典,但只有读取。这是我对手头的用例感兴趣,但不同的写入/读取比率可能会影响结果。
<强>结果
String.Intern
():2032 ms InternLocked()
:1245 ms InternConcurrent()
:458 ms InternConcurrentSafe()
:453 ms InternConcurrentSafe
与InternConcurrent
一样快的事实是有道理的,因为这些数字是在升温之后(见上文NB),所以实际上没有或只有少数几个在测试期间调用String.Copy
。
<小时/> 为了正确封装它,创建一个这样的类:
public class StringLocker
{
private readonly ConcurrentDictionary<string, string> _locks =
new ConcurrentDictionary<string, string>();
public string GetLockObject(string s)
{
return _locks.GetOrAdd(s, String.Copy);
}
}
并且在为您可能拥有的每个用例实例化一个StringLocker
之后,就像调用
lock(myStringLocker.GetLockObject(s))
{
...
<强> N.B。强>
再次思考,无需返回string
类型的对象,如果你想做的就是锁定它,那么复制字符是完全没必要的,以下将会表现优于上课。
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());
}
}
答案 1 :(得分:5)
Daniel's answer的变体......
您可以共享一小组锁,而不是为每个单独的字符串创建一个新的锁对象,根据字符串的哈希码选择要使用的锁。如果您可能拥有数千或数百万个密钥,这将意味着更少的GC压力,并且应该允许足够的粒度以避免任何严重阻塞(可能在经过一些调整后,如果需要)。
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();
这将允许您通过更改_stripes
数组中的元素数量来调整锁定粒度以满足您的特定需求。 (但是,如果你需要接近每个字符串的一个锁定粒度,那么你最好不要使用Daniel的答案。)
答案 2 :(得分:3)
我会采用实用的方法并使用虚拟变量
如果由于某种原因无法做到这一点,我会使用Dictionary<TKey, TValue>
作为键{false}和伪对象作为值并锁定该值,因为字符串不适合锁定:
key
然而,在大多数情况下,这是过度杀伤和不必要的微观优化。
答案 3 :(得分:2)
答案 4 :(得分:2)
According to the documentation,Cache类型是线程安全的。因此,不同步自己的缺点是,在创建项目时,可能会在其他线程意识到不需要创建它之前创建几次。
如果情况只是缓存常见的静态/只读事物,那么不要只是为了保存可能发生的奇怪的几次冲突而烦扰同步。 (假设碰撞是良性的。)
锁定对象不是特定于字符串的,它将特定于您需要的锁的粒度。在这种情况下,您尝试锁定对缓存的访问,因此一个对象将服务锁定缓存。锁定特定键的想法并不是锁定通常所关注的概念。
如果你想多次停止昂贵的调用,那么你可以将加载逻辑转换为新的类LoadMillionsOfRecords
,调用.Load
并按照Oded的内部锁定对象锁定一次答案。
答案 5 :(得分:0)
我在Bardock.Utils包中添加了一个由@ eugene-beresovsky answer启发的解决方案。
用法:
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());
}
}
}