wsa*_*lle 17 c# locking thread-safety higher-order-functions
更新:如果这个方法不是线程安全的,这是可以接受的,但我有兴趣学习如何使它保持线程安全.另外,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;
}
Run Code Online (Sandbox Code Playgroud)
另外,假设我key在编译时不知道所有可能的值.
如何使这个线程安全?我知道我需要在这里引入锁定,以防止1+线程将我的条件评估为真,但我不知道要锁定什么.我读过的许多关于锁定的例子(例如Jon Skeet的文章)建议使用仅用于锁定的"虚拟"私有变量.在这种情况下,这是不可能的,因为密钥在编译时是未知的.我知道我可以通过为每个人使用相同的锁来轻松地使这个线程安全key,但这可能是浪费.
现在,我的主要问题是:
有可能锁定key吗?字符串实习会有帮助吗?
在读完.NET 2.0字符串内部后,我明白我可以显式调用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;
}
}
Run Code Online (Sandbox Code Playgroud)
上面的实现线程安全吗?
Eug*_*sky 21
@ wsanville自己的解决方案的问题,部分提到:
String.Intern锁定模式) - 请注意,即使它们位于不同的AppDomain中,这包括对同一个interned字符串的锁定,可能导致跨AppDomain死锁String.Intern() 是慢的要解决所有这三个问题,您可以实现自己Intern() 的特定锁定目的,即不要将其用作全局通用字符串内插器:
private static readonly ConcurrentDictionary<string, string> concSafe =
new ConcurrentDictionary<string, string>();
static string InternConcurrentSafe(string s)
{
return concSafe.GetOrAdd(s, String.Copy);
}
Run Code Online (Sandbox Code Playgroud)
我调用了这个方法...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;
}
Run Code Online (Sandbox Code Playgroud)
基准
100个线程,每个线程随机选择5000个不同的字符串(每个包含8个数字)中的一个50000次,然后调用相应的实习方法.充分预热后的所有数值.这是4核i5上的Windows 7,64位.
注意预热上述设置意味着在预热后,不会对相应的实习词典进行任何写入,而只会读取.这是我对手头的用例感兴趣,但不同的写入/读取比率可能会影响结果.
结果
String.Intern():2032毫秒InternLocked():1245毫秒InternConcurrent():458毫秒InternConcurrentSafe():453毫秒鉴于这些数字在升温后的事实(见上文NB),这个事实InternConcurrentSafe是InternConcurrent有意义的,所以实际上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);
}
}
Run Code Online (Sandbox Code Playgroud)
并且在StringLocker为每个用例实例化一个之后,就像调用一样简单
lock(myStringLocker.GetLockObject(s))
{
...
Run Code Online (Sandbox Code Playgroud)
NB
再想一想,如果你想要做的只是锁定它就没有必要返回一个类型的对象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());
}
}
Run Code Online (Sandbox Code Playgroud)
丹尼尔答案的一个变种......
您可以共享一小组锁定,而不是为每个单独的字符串创建一个新的锁定对象,根据字符串的哈希码选择要使用的锁定.如果您可能拥有数千或数百万个密钥,这将意味着更少的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();
Run Code Online (Sandbox Code Playgroud)
这将允许您通过更改_stripes数组中的元素数量来调整锁定粒度以满足您的特定需求.(但是,如果你需要接近每个字符串一个锁定的粒度,那么你最好不要使用Daniel的答案.)