我有以下功能,旨在" memoize"无论证的函数。意思是只调用一次函数,然后在其他时间返回相同的结果。
private static Func<T> Memoize<T>(Func<T> func)
{
var lockObject = new object();
var value = default(T);
var inited = false;
return () => {
if (inited)
return value;
lock (lockObject) {
if (!inited) {
value = func();
inited = true;
}
}
return value;
};
}
我可以确定,如果一个帖子是&#34; inited == true&#34;在锁定之外,它将读取&#34;值&#34;这是在&#34; inited&#34;之前写的。被设为真?
注意:Double-checked locking in .NET涵盖它应该工作的事实,这个问题主要是检查我的实现是否正确并且可能会有更好的替代方案。
答案 0 :(得分:5)
不,因为inited
不是volatile
。 volatile
为您提供内存释放并获取您需要的围栏,以便建立正确的发生之前关系。
如果在inited
设置为true之前没有释放围栏,那么在另一个线程读取value
并将其视为inited
时,inited
可能无法完全写入true,这可能导致返回一个半构造的对象。类似地,如果在第一次检查中读取inited
之前有释放围栏但没有相应的获取围栏,那么该对象可能是完全构造的,但是看到{{1的CPU核心因为还没有看到value
被写入的记忆效应(高速缓存一致性并不一定要求在其他核心上依次看到连续写入的影响)。这将再次潜在地导致返回半构造的对象。
顺便说一句,这是已经非常详细记录的双重检查锁定模式的实例。
我建议使用volatile
显式创建自己的类,而不是使用捕获局部变量的lambda(这会使编译器生成一个隐式类来保存非易失性字段中的封闭变量)。申请value
。
private class Memoized<T>
{
public T value;
public volatile bool inited;
}
private static Func<T> Memoize<T>(Func<T> func)
{
var memoized = new Memoized<T>();
return () => {
if (memoized.inited)
return memoized.value;
lock (memoized) {
if (!memoized.inited) {
memoized.value = func();
memoized.inited = true;
}
}
return memoized.value;
};
}
当然,正如其他人提到的那样Lazy<T>
就是出于这个目的。使用它而不是自己动手,但了解事物背后的理论总是一个好主意。
答案 1 :(得分:2)
我认为您最好使用标准Lazy<T>
class来实现您需要的功能,例如:
private static Func<T> Memoize<T>(Func<T> func)
{
var lazyValue = new Lazy<T>(func, isThreadSafe: true);
return () => lazyValue.Value;
}
答案 2 :(得分:1)
不,该代码不安全。编译器可以自由地将写入重新排序到value
和inited
;内存系统也是如此。这意味着另一个帖子可能会将inited
设置为true
,而value
仍处于默认状态。
此模式称为双重检查锁定,Albahari在Lazy Initialization下讨论。建议的解决方案是使用内置的Lazy<T>
类。等效的实现如下:
private static Func<T> Memoize<T>(Func<T> func)
{
var lazy = new Lazy<T>(func);
return () => lazy.Value;
}