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

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,我知道上面提供的示例不是线程安全的。

Stu*_*tLC 6

为了缓存昂贵调用的(确定性)结果,请使用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 的单个实例。