如何在.NET中冻结冰棒(使类不可变)

Mar*_*cus 62 .net c# multithreading

我正在设计一个类,我希望在主线程完成配置之后只读它,即"冻结"它.Eric Lippert将这种冰棍称为不变性.冻结后,可以同时访问多个线程进行读取.

我的问题是如何以线程安全的方式编写这个实际有效的方法,即不要试图不必要地聪明.

尝试1:

public class Foobar
{
   private Boolean _isFrozen;

   public void Freeze() { _isFrozen = true; }

   // Only intended to be called by main thread, so checks if class is frozen. If it is the operation is invalid.
   public void WriteValue(Object val)
   {
      if (_isFrozen)
         throw new InvalidOperationException();

      // write ...
   }

   public Object ReadSomething()
   {
      return it;
   }
}
Run Code Online (Sandbox Code Playgroud)

Eric Lippert似乎认为这篇文章可以.我知道写入具有释放语义,但据我所知,这仅适用于排序,并不一定意味着所有线程都会在写入后立即看到该值.谁能证实这一点?这意味着这个解决方案不是线程安全的(当然这可能不是唯一的原因).

尝试2:

以上,但Interlocked.Exchange用于确保实际发布的值:

public class Foobar
{
   private Int32 _isFrozen;

   public void Freeze() { Interlocked.Exchange(ref _isFrozen, 1); }

   public void WriteValue(Object val)
   {
      if (_isFrozen == 1)
         throw new InvalidOperationException();

      // write ...
   }
}
Run Code Online (Sandbox Code Playgroud)

这里的优点是我们确保发布价值而不会在每次阅读时承受开销.如果在写入_isFrozen之前没有移动任何读取,因为Interlocked方法使用完整的内存屏障,我猜这是线程安全的.但是,谁知道编译器会做什么(并且根据C#规范的3.10节看起来非常多),所以我不知道这是否是线程安全的.

尝试3:

也可以使用阅读Interlocked.

public class Foobar
{
   private Int32 _isFrozen;

   public void Freeze() { Interlocked.Exchange(ref _isFrozen, 1); }

   public void WriteValue(Object val)
   {
      if (Interlocked.CompareExchange(ref _isFrozen, 0, 0) == 1)
         throw new InvalidOperationException();

      // write ...
   }
}
Run Code Online (Sandbox Code Playgroud)

绝对是线程安全的,但是每次读取都必须进行比较交换似乎有点浪费.我知道这个开销可能很小,但我正在寻找一种合理有效的方法(尽管可能就是这样).

尝试4:

使用volatile:

public class Foobar
{
   private volatile Boolean _isFrozen;

   public void Freeze() { _isFrozen = true; }

   public void WriteValue(Object val)
   {
      if (_isFrozen)
         throw new InvalidOperationException();

      // write ...
   }
}
Run Code Online (Sandbox Code Playgroud)

但乔·达菲宣称" 说不稳定 ",所以我不认为这是一个解决方案.

尝试5:

锁定一切,似乎有点矫枉过正:

public class Foobar
{
   private readonly Object _syncRoot = new Object();
   private Boolean _isFrozen;

   public void Freeze() { lock(_syncRoot) _isFrozen = true; }

   public void WriteValue(Object val)
   {
      lock(_syncRoot) // as above we could include an attempt that reads *without* this lock
         if (_isFrozen)
            throw new InvalidOperationException();

      // write ...
   }
}
Run Code Online (Sandbox Code Playgroud)

同样看起来绝对是线程安全的,但是比使用上面的Interlocked方法有更多的开销,所以我倾向于尝试3而不是这个.

然后我可以提出至少一些(我相信还有更多):

尝试6:使用Thread.VolatileWriteThread.VolatileRead,但这些可能有点偏重.

尝试7:使用Thread.MemoryBarrier,似乎有点内部.

尝试8:创建一个不可变的副本 - 不想这样做

总结:

  • 您将使用哪种尝试以及为什么(或者如果完全不同,您将如何进行)?(即,一旦然后同时读取值,发布值的最佳方式是什么,同时合理有效而不过度"聪明"?)
  • .NET的内存模型"释放"写入的语义是否暗示所有其他线程都看到更新(缓存一致性等)?我一般不想过多考虑这个问题,但了解一下我很高兴.

编辑:

也许我的问题不明确,但我特别关注上述尝试为何好或坏的原因.请注意,我在这里谈论的是一个单独的编写器在任何并发读取之前写入然后冻结的情况.我相信尝试1是可以的,但我想确切地知道为什么(因为我想知道是否可以以某种方式优化读取,例如).我不太关心这是否是好的设计实践,而是更关心它的实际线程方面.


非常感谢收到问题的答复,但我选择将此标记为自己的答案,因为我觉得给出的答案并不能完全回答我的问题,我不想给访问该网站的任何人留下印象.答案是正确的,因为它因为赏金到期而被自动标记.此外,我不认为投票数最多的答案是以压倒多数投票的,而不是自动将其标记为答案.

我仍然倾向于尝试#1正确,但是,我会喜欢一些权威的答案.我理解x86有一个强大的模型,但我不想(也不应该)为特定的架构编写代码,毕竟这是关于.NET的好东西之一.

如果您对答案有疑问,请选择其中一种锁定方法,也许使用此处显示的优化方法,以避免对锁定进行大量争用.

小智 25

也许稍微偏离主题但只是出于好奇:)你为什么不使用"真正的"不变性?例如,使Freeze()返回一个不可变的副本(没有"写入方法"或任何其他更改内部状态的可能性)并使用此副本而不是原始对象.您甚至可以在不更改状态的情况下进行操作,并在每次写入操作中返回一个新副本(具有已更改的状态)(afaik字符串类可以正常工作)."真正的不变性"本质上是线程安全的.

  • 真正的不变性是前进的方向.所有这些其他解决方案都是荒谬的. (2认同)
  • 实现不可变对象的整个副本不是一个可维护的解决方案。我相信冰棒在您不能使用 readonly 时很有用,因为在对象构造期间变量的值不可用。如果有人试图更改该值,则冰棒的好处是通过抛出异常使您的假设显而易见。它不如像“readonly”这样的编译时错误好,但我们应该支持小的、可维护的解决方案。毕竟,不变性是关于去除复杂性,我们为什么要以这种方式重新添加它? (2认同)

csh*_*net 8

我投票尝试5,使用锁(this)实现.

这是使这项工作最可靠的方法.可以使用读写器锁,但收益很少.只需使用普通锁即可.

如有必要,您可以通过先检查_isFrozen然后锁定来改善"冻结"性能:

void Freeze() { lock (this) _isFrozen = true; }
object ReadValue()
{
    if (_isFrozen)
        return Read();
    else
        lock (this) return Read();
}
void WriteValue(object value)
{
    lock (this)
    {
        if (_isFrozen) throw new InvalidOperationException();
        Write(value);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 使用你的syncRoot代码; 不要'锁定(这)`.请参阅[为什么锁定(此){...}不好?](http://stackoverflow.com/a/251668/161457) (4认同)
  • -1任何使用lock()都会阻止多个并发读者.如果您可能同时访问多于1个或2个线程/任务,那就太糟糕了.如果你可能有32个线程同时读取,真的很糟糕. (2认同)

小智 5

如果您真正创建,填充并冻结对象,然后再将其显示给其他线程,那么您不需要任何特殊处理线程安全性(.NET的强大内存模型已经是您的保证),因此解决方案1是有效.

但是,如果您将未冻结的对象提供给另一个线程(或者如果您在不知道用户将如何使用它的情况下简单地创建您的类),则使用该版本,返回新的完全不可变实例的解决方案可能更好.在这种情况下,Mutable实例类似于StringBuilder,而不可变实例就像字符串一样.如果您需要额外的保证,可变实例可以检查其创建者线程并在从任何其他线程使用时抛出异常(在所有方法中......以避免可能的部分读取).


Chr*_*isW 1

该东西是否被构造并写入,然后永久冻结并多次读取?

或者你冷冻、解冻、再冷冻多次?

如果是前者,那么也许“被冻结”检查应该在读取器方法中而不是写入器方法中(以防止它在冻结之前读取)。

或者,如果是后者,那么您需要注意的用例是:

  • 主线程调用writer方法,发现没有冻结,于是开始写入
  • 在写入完成之前,有人尝试冻结对象然后从中读取,而另一个(主)线程仍在写入

在后一种情况下,谷歌显示了许多针对多个读者单个作者的结果,您可能会觉得有趣。

  • @Marcus如果它被永久冻结,并且如果同一个(主)线程是调用“Freeze”方法和“WriteValue”方法的唯一线程,那么您的尝试#1是最简单和最便宜的解决方案,并且足够了(假设有另一种机制可以防止其在冻结之前被读取,例如,在冻结之前不向读取器线程公开/发布实例)。 (2认同)