以下是该方案。我有一个将由多个线程(ASP.NET)访问的类,可以将结果存储在一次写入,多次读取的缓存中。此高速缓存的对象是无法作为静态初始化程序的一部分执行的操作的结果,但必须等待第一次执行。所以我实现了一个简单的null检查,如下所示。我知道如果两个线程在同一时刻点击此检查,我将计算两次ExpensiveCalculation,但这不是世界末日。我的问题是,我是否需要担心由于优化或其他线程缓存,静态_cachedResult仍然被其他线程视为null。一旦写入,该对象只被读取,所以我认为不需要全面锁定。
public class Bippi
{
private static ExpensiveCalculation _cachedResult;
public int DoSomething(Something arg)
{
// calculate only once. recalculating is not harmful, just wastes time.
if (_cachedResult == null);
_cachedResult = new ExpensiveCalculation(arg);
// additional work with both arg and the results of the precalculated
// values of _cachedResult.A, _cachedResult.B, and _cachedResult.C
int someResult = _cachedResult.A + _cachedResult.B + _cachedResult.C + arg.ChangableProp;
return someResult;
}
}
public class ExpensiveCalculation
{
public int A { get; private set; }
public int B { get; private set; }
public int C { get; private set; }
public ExpensiveCalculation(Something arg)
{
// arg is used to calculate A, B, and C
}
}
附加说明,这是在.NET 4.0应用程序中。
答案 0 :(得分:6)
我的问题是,我是否需要担心由于优化或其他线程缓存,静态_cachedResult仍被其他线程视为null。
是的,你这样做。这是volatile
存在的主要原因之一。
值得一提的是,无争议的锁定会增加完全可忽略不计的性能成本,因此我们没有理由仅仅lock
进行空检查和资源生成,因为它&#39几乎肯定不会导致任何性能问题,并使程序更容易推理。
最好的解决方案是完全避免这个问题,并使用专门设计的更高级别的抽象来解决您遇到的确切问题。在这种情况下,这意味着Lazy
。您可以创建一个Lazy
对象来定义如何创建昂贵的资源,在需要对象的任何地方访问它,并且Lazy
实现负责确保资源的创建不超过一次,并且它被正确地暴露给要求所述资源的代码,并且它被有效地处理。
答案 1 :(得分:0)
你不需要volatile,你 - 尤其是 - 需要一个内存屏障,以便处理器缓存同步。
答案 2 :(得分:0)
我认为您可以完全乐观地避免锁定,但避免volatile
性能损失。您可以分两步测试可空性。
object readonly _cachedResultLock = new object();
...
if (_cachedResult == null)
{
lock(_cachedResultLock)
{
if (_cachedResult == null)
{
_cachedResult = new ExpensiveCalculation(arg);
}
}
}
这里大部分时间你都无法锁定,也不会序列化访问。您可以仅在第一次访问时序列化访问权限 - 但会保证不会浪费工作(尽管可能会导致另一个线程在第一次完成ExpensiveCalculation
时等待一段时间。)