【问题标题】:Threadsafe Lazy Class线程安全懒惰类
【发布时间】:2009-12-02 16:05:18
【问题描述】:

我有 Lazy 类懒惰地评估一个表达式:

public sealed class Lazy<T>
{
    Func<T> getValue;
    T value;

    public Lazy(Func<T> f)
    {
        getValue = () =>
            {
                lock (getValue)
                {
                    value = f();
                    getValue = () => value;
                }
                return value;
            };
    }

    public T Force()
    {
        return getValue();
    }
}

基本上,我试图避免在评估对象后锁定对象的开销,因此我在调用时将 getValue 替换为另一个函数。

显然在我的测试中有效,但我无法知道它是否会在生产中爆炸。

我的班级是线程安全的吗?如果没有,可以做些什么来保证线程安全?

【问题讨论】:

    标签: c# multithreading thread-safety


    【解决方案1】:

    你不能通过使用标志或保护值来完全忽略重新评估函数的实际值吗?即:

    public sealed class Lazy<T>
    {
        Func<T> f;
        T value;
        volatile bool computed = false;
        void GetValue() { lock(LockObject) { value = f();  computed = true; } }
    
        public Lazy(Func<T> f)
        {
            this.f = f;
        }
    
        public T Force()
        {
            if (!computed) GetValue();
            return value;
        }
    }
    

    【讨论】:

    • 您的代码不是线程安全的,优化器可能会重新排序您代码中的写入或读取。为了使您的代码安全,您必须将“bool computed”标记为 volatile。通常,每当您读取可能从另一个线程设置的没有锁定的变量时,您需要考虑内存模型以及需要障碍或易失性读/写的位置。
    • +1, +answer:我不喜欢戴复杂的手套,这正是我不应该被允许触摸代码的原因 :) 如上所述,我认为将标志标记为 volatile将以更少的“魔法”获得我想要的结果。
    • @Daniel:如果变量“computed”已经包含在 lock 语句中,为什么需要将其标记为 volatile?出于同样的原因,变量“value”是否需要标记为 volatile?
    • "computed" 需要标记为 volatile,因为它是在没有适当锁定的情况下访问的:读取变量时没有锁定。将“已计算”标记为 volatile 可确保对该线程的写入在该线程发出的先前写入之后执行,并且在读取线程发出其他读取之前读取它。有关详细信息,请参阅albahari.com/threading/part4.aspx#_NonBlockingSynch。 "value" 不需要 volatile,因为它是在已经观察到 "computed" 为真之后读取的。
    【解决方案2】:

    您的代码有一些问题:

    1. 您需要一个对象来执行锁定。不要锁定已更改的变量 - 锁定始终处理对象,因此如果 getValue 更改,多个线程可能会同时进入锁定部分。

    2. 如果有多个线程在等待锁,所有线程都将依次评估函数 f()。您必须在锁内部检查该函数尚未评估。

    3. 即使在解决了上述问题之后,您也可能需要一个内存屏障,以确保仅在将新值存储到内存后才替换委托。

    但是,我会改用 Konrad Rudolph 的标志方法(只要确保您不要忘记为此所需的“易失性”)。这样一来,您就不需要在检索到值时调用委托(委托调用非常快;但它们不像简单地检查布尔值那么快)。

    【讨论】:

      【解决方案3】:

      我不完全确定你想用这段代码做什么,但我 just published an article on The Code Project 正在构建一种“惰性”类,它会自动、异步地调用一个工作函数并存储它的值。

      【讨论】:

      • 非常有趣的文章 :) 但它并不是大多数人感兴趣的意义上的“懒惰”。两个用例脱颖而出:1)大多数 ORM 使用延迟加载来防止实例化数据库整个对象图一下子在记忆中。 2)一整类不可变的数据结构(值得注意的流和队列的一些实现)依赖于某些摊销性能特征的惰性。
      【解决方案4】:

      这看起来更像是一种缓存机制,而不是“惰性求值”。此外,不要更改lock 块内的锁定引用的值。使用临时变量来锁定。

      你现在的等待在很多情况下都可以工作,但是如果你有两个不同的线程尝试按这个顺序计算表达式:

      Thread 1
      Thread 2
      Thread 1 completes
      

      线程 2 永远不会完成,因为线程 1 将释放与用于获取锁的引用不同的锁(更准确地说,他将释放一个不存在的锁,因为新创建的引用从未被锁定首先),并且释放阻塞线程 2 的原始锁。

      虽然我不完全确定这会做什么(除了对表达式执行同步评估和缓存结果),但这应该会更安全:

      public sealed class Lazy<T>
      {
          Func<T> getValue;
          T value;
          object lockValue = new object();
      
          public Lazy(Func<T> f)
          {
              getValue = () =>
                  {
                      lock (lockValue)
                      {
                          value = f();
                          getValue = () => value;
                      }
                      return value;
                  };
          }
      
          public T Force()
          {
              return getValue();
          }
      }
      

      【讨论】:

      • 现在确保 f() 不会被同时调用;但它仍然可以被多次调用。
      • @Daniel:是的,你是对的。为了防止这种情况,需要将锁移至Force 方法。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-08-19
      • 1970-01-01
      • 2022-01-09
      相关资源
      最近更新 更多