c# - 如何在 .NET 中卡住冰棒(使类不可变)

标签 c# .net multithreading

我正在设计一个我希望在主线程完成配置后只读的类,即“卡住”它。埃里克·利珀特 (Eric Lippert) 称此 popsicle不变性。卡住后,可以被多个线程并发访问读取。

我的问题是如何以实际高效的线程安全方式编写此代码,即不尝试变得不必要的聪明。

尝试 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;
   }
}

Eric Lippert 似乎暗示这在 this 中是可以的邮政。
我知道写入具有释放语义,但据我所知,这仅与排序有关,并不一定意味着所有线程都会在写入后立即看到该值。任何人都可以证实这一点吗?这意味着该解决方案不是线程安全的(当然这可能不是唯一的原因)。

尝试 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 ...
   }
}

这里的优点是我们可以确保发布该值而不会在每次读取时产生开销。如果在写入 _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 ...
   }
}

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

尝试 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 ...
   }
}

但是 Joe Duffy 声明了“sayonara volatile”,所以我不会认为这是一个解决方案。

尝试 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 ...
   }
}

似乎也绝对是线程安全的,但比使用上面的 Interlocked 方法有更多的开销,所以我更喜欢尝试 3 而不是这个。

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

尝试 6:使用 Thread.VolatileWriteThread.VolatileRead ,但这些据说有点偏重。

尝试 7:使用 Thread.MemoryBarrier ,似乎有点太内向了。

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

总结:
  • 你会使用哪种尝试,为什么(或者如果完全不同,你会怎么做)? (即,发布一个值然后并发读取的最佳方式是什么,同时又不过度“聪明”的合理效率?)
  • .NET 的内存模型“释放”写入语义是否意味着所有其他线程都会看到更新(缓存一致性等)?我一般不会想太多关于这个,但有一个理解是很好的。

  • 编辑:

    也许我的问题不清楚,但我特别在寻找上述尝试是好是坏的原因。请注意,我在这里谈论的是一个单一写入器的场景,该写入器在任何并发读取之前写入然后卡住。我相信尝试 1 是可以的,但我想确切地知道为什么(例如,我想知道读取是否可以以某种方式进行优化)。
    我不太关心这是否是好的设计实践,而更关心它的实际线程方面。

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

    我仍然倾向于尝试 #1 是正确的,但是,我希望得到一些权威的答案。我知道 x86 有一个强大的模型,但我不想(也不应该)为特定的体系结构编码,毕竟这是 .NET 的优点之一。

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

    最佳答案

    也许稍微偏离主题,但只是出于好奇:) 为什么不使用“真正的”不变性?例如使 Freeze() 返回一个不可变的副本(没有“写方法”或任何其他改变内部状态的可能性)并使用这个副本而不是原始对象。您甚至可以不更改状态并在每次写入操作时返回一个新副本(具有更改的状态)(因为字符串类可以实现此目的)。 “真正的不变性”本质上是线程安全的。

    关于c# - 如何在 .NET 中卡住冰棒(使类不可变),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13691612/

    相关文章:

    c# - 如何使用 Html Agility Pack 获取 img/src 或 a/hrefs?

    c# - 在基页类中使用用户控件

    android - 使 Android NFC onTagDiscovered 回调线程保持 Activity 状态

    C++。 std::condition_variable 和多个等待线程

    java - 同步块(synchronized block)锁定对象并等待/通知

    c# - 如何判断我的对象的值是 float 还是 int?

    c# - 从字符串末尾删除空格的最佳方法

    c# - winform之间传递变量

    c# - 检查异步方法的调用 Received()

    .net - 从结构更改为类是否安全