sta*_*k92 3 c# singleton thread-safety
我对使用单例非常陌生,并且很难理解 C# 中单例的惰性实现。
假设我有一个最初为空/空的字符串,当有人对该字符串进行 get 调用时,我必须仅在它为空/空时计算该字符串,否则返回现有字符串。
我的正常实现是这样的。
public class A
{
private string str = null;
public A()
{
}
public string GetStr()
{
if(String.IsNullOrEmpty(str))
{
str = CalculateStr();
}
return str;
}
}
Run Code Online (Sandbox Code Playgroud)
如何实现上述示例的线程安全版本?
编辑 #1:CalculateStr()可以返回空/空字符串。如果是这种情况,我们需要重新计算下一次。
编辑 #2:用例是变量 str 应该是线程安全的,并且只有在它不是 null/空时才应该计算。
编辑 #3:我不知道它是否称为 singleton,我知道上面提供的示例不是线程安全的。
为了缓存昂贵调用的(确定性)结果,请使用Lazy<T>- 这有一个可选LazyThreadSafetyMode参数,允许您指定如何解决并发问题。
更新 - 假设CalculateStr不是静态的
public class A
{
private readonly Lazy<string> _lazyStr;
public A()
{
// Provide a factory method
_lazyStr = new Lazy<string>(() => CalculateStr());
}
public string GetStr()
{
// Lazy retrieval of the value, invokes factory if needed.
return _lazyStr.Value;
}
public string CalculateStr()
{
// Expensive method goes here. Track to ensure method only called once.
Console.WriteLine("Called");
return "Foo";
}
}
Run Code Online (Sandbox Code Playgroud)
行为如下,即:
GetStr,则CalculateStr完全避免(假定昂贵的)调用GetStr被多次调用,则该值将被缓存并重用。GetStr在第一次需要时并发调用,那么这LazyThreadSafetyMode将允许您决定如何处理并发。您可以序列化调用(使用ExecutionAndPublication,默认值),即阻塞直到其中一个线程创建单个实例,或者您可以在所有线程上同时调用工厂,并且调用结果之一将被缓存 ( PublicationOnly)。对于昂贵的调用,您不会想要使用PublicationOnly.更新 - 如果 CalculateStr 返回 null 或为空,则“重试”
请注意,OP 的更新要求并不完全符合经典的“延迟实例化”模式 - 看似CalculateStr方法调用不可靠,有时会返回 null。因此,OP 的要求是缓存来自方法的第一个非空响应,但如果初始响应为空,则不重试。而不是使用Lazy,我们需要自己做这件事。这是一个双重检查的锁实现。
public class A
{
private string _cachedString = null;
private object _syncLock = new object();
public string GetStr()
{
if (_cachedString == null)
{
lock(_syncLock)
{
if (_cachedString == null)
{
var test = CalculateStr();
if (!string.IsNullOrEmpty(test))
{
_cachedString = test;
}
return test;
}
}
}
return _cachedString;
}
public string CalculateStr()
{
// Unreliable, expensive method here.
// Will be called more than once if it returns null / empty.
Console.WriteLine("Called");
return "Foo";
}
}
Run Code Online (Sandbox Code Playgroud)
请注意,以上都不需要单例实例 -A可以根据需要调用尽可能多的实例,并且每个A实例将(最终)缓存从CalculateStr. 如果需要单例,则共享A实例,或者使用 IoC 容器来控制 A 的单个实例。