我正在寻找一个惰性的、线程安全的实现来缓存昂贵计算的第一个非空结果



我对使用单例很陌生,很难理解 C# 中单例的惰性实现。

假设我有一个最初为 null/空的字符串,当有人对该字符串进行 get 调用时,我必须仅在字符串为 null/空时才计算该字符串,否则返回现有字符串。

我的正常实现如下所示。

public class A
{
private string str = null;
public A()
{
}
public string GetStr()
{
if(String.IsNullOrEmpty(str)) 
{
str = CalculateStr();
}
return str;
}
}

如何实现上述示例的线程安全版本?

编辑#1:CalculateStr()可以返回空/空字符串。如果是这种情况,我们需要在下次重新计算。

编辑 #2:用例是变量 str 应该是线程安全的,并且只有在它不为 null/空时才应计算。

编辑#3:我不知道它是否称为单例,我知道上面提供的示例不是线程安全的。

要缓存昂贵调用的(确定性(结果,请使用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";
}
}

行为如下,即:

  • 如果没有任何东西调用GetStr,那么完全避免(假定昂贵的(对CalculateStr的调用
  • 如果多次调用GetStr,则会缓存并重用该值。
  • 如果两个或多个线程在第一次需要时同时调用GetStr,则LazyThreadSafetyMode将允许您决定如何处理并发。您可以序列化调用(使用ExecutionAndPublication,默认值(,即阻塞直到其中一个线程创建单个实例,或者您可以在所有线程上并发调用工厂,并且其中一个调用结果将被缓存(PublicationOnly(。对于昂贵的电话,您不会希望使用PublicationOnly

更新 - 如果 CalculateStr 返回空值或空值,则"重试">

请注意,OP 的更新要求不太符合经典的"惰性实例化"模式 - 似乎CalculateStr方法调用不可靠,有时会返回 null。因此,OP 的要求是缓存来自该方法的第一个非空响应,但如果初始响应为 null,则不重试。而不是使用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";
}
}

请注意,以上都不需要单例实例 - 可以根据需要调用尽可能多的A实例,并且每个A实例(最终(将缓存从CalculateStr返回的单个非空值。如果需要单一实例,请共享A实例,或使用 IoC 容器控制 A 的单个实例。

在 .NET Core 中,现代 C# 中惰性单例的最简单实现如下所示:

public class A
{
public static readonly string LazyStr = CalculateStr();
private static string CalculateStr(){}
}

LazyStr变量只会在您第一次需要它的时候初始化(因为静态只读关键字(,之后,它将始终相同。

试试这个简单的例子:

class Program
{
static void Main(string[] args)
{
Console.WriteLine($"Start at {DateTime.Now}");
Console.ReadKey();
Console.WriteLine(A.LazyString);
Console.ReadKey();
Console.WriteLine(A.LazyString);
Console.ReadKey();
}
}
public class A
{
public static readonly String LazyString = CalculateString();
private static string CalculateString()
{
return DateTime.Now.ToString();
}
}

首先,你的str应该是静态的,才能成为"单例"。 其次,您可以在 https://learn.microsoft.com/en-us/dotnet/framework/performance/lazy-initialization 中使用Lazy<T>

并像这样定义单例

private static readonly Lazy<string>
str =
new Lazy<string>
(() => CalculateStr());

通过使用Lazy<T>您可以在不使用锁定的情况下存档线程安全。

最新更新