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

我正在devise一个我希望在主线程完成configuration后只读的类,即“冻结”它。 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; } } 

埃里克·利珀特似乎表示这将是好的在这篇文章。 我知道写有释放语义,但据我所知这只是有关sorting ,并不一定意味着所有线程将在写入后立即看到值。 任何人都可以确认吗? 这意味着这个解决scheme不是线程安全的(当然这可能不是唯一的原因)。

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

但乔·达菲宣布“ sayon​​ara易变 ”,所以我不会认为这是一个解决scheme。

尝试5:

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

 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在这一个。

然后,我可以至less有更多的(我敢肯定还有更多):

尝试6:使用Thread.VolatileWriteThread.VolatileRead ,但这些都是沉重的一面。

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

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

总结:

  • 你会使用哪个尝试,为什么(或者如果完全不同,你会怎么做)? (即一次发布价值的最佳方式是什么,然后合理有效而不过分“聪明”?)
  • .NET的内存模型“释放”写入语义意味着所有其他线程看到更新(caching一致性等)? 我一般不想过多考虑这个问题,但理解起来很好。

编辑:

也许我的问题并不清楚,但我特别关注的是为什么上述尝试是好的或坏的原因。 请注意,我在这里谈论的是一个单一写入器的情况,写入然后在任何并发读取之前冻结。 我相信尝试1是好的,但是我想知道为什么(例如,我想知道读取是否可以通过某种方式进行优化)。 我不在乎这是否是好的devise实践,而是更多关于实际的线程方面。


非常感谢所收到的问题的答复,但是我select将其标记为自己的答案,因为我觉得所给出的答案不能完全回答我的问题,而且我不想给任何访问该网站的人留下印记答案是正确的,因为它是自动标记为由于赏金到期。 此外,我不认为拥有最高票数的答案被压倒多数,不足以自动将其标记为答案。

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

如果你对这个答案有疑问,那么可以select其中一种locking方法,或许可以通过这里显示的优化来避免锁的争用。

也许有些话题,但只是出于好奇:)为什么你不使用“真正的”不变性? 例如,使Freeze()返回一个不可变的副本(没有“写入方法”或任何其他可能性来改变内部状态),并使用这个副本,而不是原始对象。 你甚至可以在不改变状态的情况下进行操作,而是在每次写入操作时返回一个新的副本(改变状态)(afaikstring类可以工作)。 “真正的不变性”本质上是线程安全的。

我投了尝试5,使用锁(这个)的实现。

这是做这项工作的最可靠的手段。 可以使用读卡器/写卡器锁,但获得很less的收益。 只要去使用正常的锁。

如有必要,您可以先通过检查_isFrozen然后locking来提高“冻结”性能:

 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); } } 

如果你真的在创build,填充和冻结对象之前,显示给其他线程,那么你不需要任何特殊的处理线程安全(强大的内存模型的.NET已经是你的保证),所以解决scheme1是有效。

但是,如果您将解冻的对象提供给另一个线程(或者如果您简单地创build类而不知道用户将如何使用它),那么使用该版本的解决scheme返回一个新的完全不可变的实例可能会更好。 在这种情况下,Mutable实例就像StringBuilder,不可变实例就像string。 如果你需要额外的保证,可变实例可能会检查它的创build者线程,并抛出exception,如果从任何其他线程使用(在所有方法…以避免可能的部分读取)。

尝试2在x86和其他具有强大内存模型的处理器上是线程安全的,但是我怎样做才能使线程安全成为消费者的问题,因为在消耗的代码中没有办法有效地执行它。 考虑:

 if(!foo.frozen) { foo.apropery = "avalue"; } 

frozen财产的线程安全和apropery的setter中的守护代码并不重要,因为即使它们是完全线程安全的,你仍然有一个竞争条件。 相反,我会写它像

 lock(foo) { if(!foo.frozen) { foo.apropery = "avalue"; } } 

并没有固有的线程安全的属性。

#1 – 读者没有线程安全 – 我相信问题会在读者端,而不是写者(代码未显示)
#2 – 阅读器不是线程安全的 – 与#1相同
#3 – 很有希望,读取检查可以在大多数情况下进行优化(当CPU高速caching同步时)

尝试3:

还要使用联锁进行读取。

 public class Foobar { private object _syncRoot = new object(); private int _isFrozen = 0; // perf compiler warning, but training code, so show defaults // Why Exchange to 1 then throw away result. Best to just increment. //public void Freeze() { Interlocked.Exchange(ref _isFrozen, 1); } public void Freeze() { Interlocked.Increment(ref _isFrozen); } public void WriteValue(Object val) { // if this core can see _isFrozen then no special lock or sync needed if (_isFrozen != 0) throw new InvalidOperationException(); lock(_syncRoot) { if (_isFrozen != 0) throw new InvalidOperationException(); // the 'throw' is 100x-1000x more costly than the lock, just eat it _val = val; } } public object Read() { // frozen is one-way, if one-way state has been published // to my local CPU cache then just read _val. // There are very strange corner cases when _isFrozen and _val fields are in // different cache lines, but should be nearly impossible to hit unless // dealing with very large structs (make it more likely to cross // 4k cache line). if (_isFrozen != 0) return _val; // else lock(_syncRoot) { // _isFrozen is 0 here if (_isFrozen != 0) // if _isFrozen is 1 here we just collided with writer using lock on other thread, or our CPU cache was out of sync and lock() forced the dirty cache line to be read from main memory return _val; throw new InvalidOperationException(); // throw is 100x-1000x more expensive than lock, eat the cost of lock } } } 

Joe Duffy关于“volatile已经死亡”的post,我认为是在他的下一代CLR / OS架构和ARM上的CLR上。 我们这些做多核x64 / x86我认为易变性是好的。 如果性能是主要关心,我build议你测量上面的代码,并将其与易失性进行比较。

与其他人发布的答案不同,如果你有很多读者(3个或更多的线程可能同时读取同一个对象),我不会直接跳到lock()。 但是在你的示例中,碰撞发生时,你将混合敏感问题与exception混合,这没有多大意义。 如果你使用exception,那么你也可以使用其他更高级的构造。

如果你想完全安全,但需要优化大量的并发读者更改锁()/监视器ReaderWriterLockSlim。

.NET有新的原语来处理发布值。 看看Rx。 在某些情况下,它可以非常快速和无锁(我认为它们使用与上面类似的优化)。

如果写入多次但只保留一个值 – 在Rx中是“new ReplaySubject(bufferSize:1)”。 如果你尝试它,你可能会惊讶它有多快。 同时,我赞赏你试图学习这一级别的细节。

如果你想无锁地克服你对Thread.MemoryBarrier()的厌恶。 这是非常重要的。 但是它和Joe Duffy所描述的一样具有易变的性质 – 它被devise成提示编译器和CPU来防止对内存读取进行重新sorting(这在CPU中需要很长时间,所以当没有内存读取时,它们会被激烈地重新sorting提示存在)。 当这种重新sorting与C​​LR结构(如函数的自动内联)结合使用时,可以在内存和寄存器级别看到非常令人惊讶的行为。 MemoryBarrier()只是禁用CPU和CLR大部分时间使用的单线程内存访问假设。

也许我的问题并不清楚,但我特别关注的是为什么上述尝试是好的或坏的原因。 请注意,我在这里谈论的是一个单一写入器的情况,写入然后在任何并发读取之前冻结。 我相信尝试1是好的,但是我想知道为什么(例如,我想知道读取是否可以通过某种方式进行优化)。 我不在乎这是否是好的devise实践,而是更多关于实际的线程方面。

好吧,现在我更好地理解你在做什么,并寻找答案。 请允许我详细说明我以前的回答,通过首先解决您的每一个尝试来推动使用锁。

尝试1:

使用一个没有任何forms的同步原语的简单类的方法在你的例子中是完全可行的。 由于“创作”线程是在变化状态期间唯一可以访问此类的线程,因此应该是安全的。 如果只有另一个线程有可能在类被“冻结”之前访问,你需要提供同步。 从本质上说,线程不可能拥有从未见过的东西的caching。

除了具有此列表内部状态的caching副本的线程之外,还有一个您应该关心的并发问题。 你应该考虑编写线程写入重新sorting。 你的例子解决scheme没有足够的代码来解决这个问题,但把这个“冻结”列表交给另一个线程的过程是问题的核心。 你使用Interlocked.Exchange还是写入volatile状态?

我仍然主张这不是最好的方法,因为不能保证另一个线程在变异的时候没有看到这个实例。

尝试2:

虽然尝试2不应该使用。 如果您正在使用primefaces写入成员,还应该使用primefaces读取。 我永远不会推荐一个没有另一个,因为没有读取和写入是primefaces,你没有得到任何东西。 primefaces读写的正确应用是你的“尝试3”。

尝试3:

这将保证如果一个线程试图改变一个冻结列表,就会抛出一个exception。 然而,它并没有断言读取只能在一个冻结的实例上被接受。 这,恕我直言,就像使用primefaces和非primefaces访问器访问我们的_isFrozenvariables一样糟糕。 如果你要说保护写入是重要的,那么你应该总是保证读取。 没有其他的只是“奇怪的”。

忽略了我自己写代码的感觉,这个代码可以写但不会读取,这是一个可以接受的方法,因为你的具体用途。 我有一个作家,我写,我冻结,然后我把它提供给读者。 在这种情况下你的代码工作正常。 您可以依靠_isFrozen集合上的primefaces操作来在将类交给另一个线程之前提供所需的内存屏障。

简而言之,这种方法是有效的,但是如果一个线程有一个没有被冻结的实例,它将会中断。

尝试4:

虽然这与几乎相同的尝试3(给一个作家),但有一个很大的区别。 在这个例子中,如果您在阅读器中检查_isFrozen ,那么每次访问都需要一个内存屏障。 一旦列表被冻结,这是不必要的开销。

这仍然和尝试3有同样的问题,因为在读取过程中没有对_isFrozen的状态做任何断言,所以在你的例子中的性能应该是相同的。

尝试5:

正如我所说,这是我的偏好给予修改阅读出现在我的其他答案。

尝试6:

基本上与#4是一样的。

尝试7:

你可以用Thread.MemoryBarrier来解决你的特定需求。 本质上使用来自尝试1的代码,创build实例,调用Freeze() ,添加您的Thread.MemoryBarrier ,然后共享实例(或在锁中共享)。 这应该很好,再次只在你有限的使用情况下。

尝试8:

不知道更多关于这个,我不能build议副本的成本。

概要

再次,我更喜欢使用具有一些线程保证的类,或者根本没有。 创build一个只是“部分”线程安全的类,IMO是危险的。

用一个着名的绝地大师的话来说:

要么做或不要没有尝试。

线程安全也是如此。 这个类应该是线程安全的或不是。 采取这种方法,你可以使用我尝试增加的尝试5,或使用尝试7.给出的select,我永远不会推荐#7。

所以我的build议坚决支持完全线程安全的版本。 两者之间的性能成本是非常小的,几乎不存在。 读者线程永远不会仅仅因为你有一个单一的写作者的使用场景而碰锁。 但是,如果他们这样做,正确的行为仍然是确定的。 因此,随着您的代码随着时间的推移而变化,突然间您的实例在被冻结之前就被共享了,您不会因为使您的程序崩溃而导致竞争状态。 线程安全或不安全,不要半途而废,否则总有一天你会遇到令人讨厌的惊喜。

我的首选是由多个线程共享的所有类是两种types之一:

  1. 完全不可改变。
  2. 完全线程安全。

由于冰棍列表不是一成不变的,所以不适合#1。 因此,如果你要通过线程共享对象,它应该适合#2。

希望所有这些咆哮进一步解释我的推理:)

_syncRoot

很多人都注意到我在我的locking实现上跳过了一个_syncRoot的使用。 尽pipe使用_syncRoot的原因是有效的,但并不总是必需的。 在你有一个写入器的示例用法中, lock(this)应该足够好,而不需要为_syncRoot添加另一个堆分配。

是构build和写入的东西,然后永久冻结和阅读多次?

或者你冻结和解冻并多次重新冻结?

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

或者,如果是后者,则需要注意的用例是:

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

在后一种情况下,Google显示了多个阅读器单个作者的许多结果,您可能会感兴趣。

一般来说,每个可变对象都应该有一个明确定义的“所有者”。 共享对象应该是不可变的。 直到冰冻之后,冰棍不应该被多个线程访问。

就个人而言,我不喜欢暴露的“冻结”方法的冰棍免疫forms。 我认为一个更清洁的方法是使用AsMutableAsImmutable方法(每个方法只需在适当的时候返回未修改的对象)。 这样的方法可以允许关于不变性的更强有力的承诺。 例如,如果一个“非共享的可变对象”在其AsImmutable成员被调用的时候发生了变异(这种行为与该对象被“取消共享”相反),那么副本中的数据状态可能是不确定的,返回将是不可改变的。 相反,如果一个线程冻结了一个对象,然后在另一个线程正在写入的时候假定它是不可变的,那么“不可变”的对象在被冻结并读取其值之后可能会变化。

编辑

基于进一步的描述,我会build议在监视器锁中写入对象的代码,并使冻结例程如下所示:

公共Thingie Freeze(void)//返回有问题的对象
 {
  如果(isFrozen)//私有字段
    返回这个;
  其他
    返回DoFreeze();
 }

 Thingie DoFreeze(void)
 {
  如果(Monitor.TryEnter(无论))
   {
     isFrozen = true;
    返回这个;
   }
  否则如果(isFrozen)
    返回这个;
  其他
    抛出新的InvalidOperationException(“作者使用的对象”);
 }

Freeze方法可以被任意数量的线程调用任意次数; 它应该足够短以便内联(尽pipe我没有对其进行分析),因此应该几乎没有时间来执行。 如果任何线程中的对象的第一次访问是通过Freeze方法进行的,那么在任何合理的内存模型下都应该保证正确的可见性(即使线程没有看到由创build的线程执行的对象的更新并且最初冻结了它,它将执行TryEnter ,这将保证内存屏障,并在此之后,它会注意到对象被冻结,并返回它。

如果要写入对象的代码首先获取锁,则尝试写入冻结对象可能会死锁。 如果有人愿意让这样的代码抛出一个exception,那么就使用TryEnter ,如果它不能得到锁,就抛出一个exception。

用于locking的对象应该是被冻结的对象专有的东西。 如果被冻结的对象不包含对任何东西的纯私有引用,则可以lockingthis对象,或者纯粹为了locking目的创build一个私有对象。 请注意,放弃“input”监视器锁而不清理是安全的; GC会简单的忘掉它们,因为如果锁没有引用,那么任何人都不会关心(或甚至可以问)锁是否在被放弃的时候进入。

在成本方面,我不确定下面的方法将如何做,但有点不同。 只有最初如果有多个线程试图同时写入值,他们会遇到锁。 一旦被冻结,所有以后的调用都会直接得到exception。

尝试9:

 public class Foobar { private readonly Object _syncRoot = new Object(); private object _val; private Boolean _isFrozen; private Action<object> WriteValInternal; public void Freeze() { _isFrozen = true; } public Foobar() { WriteValInternal = BeforeFreeze; } private void BeforeFreeze(object val) { lock (_syncRoot) { if (_isFrozen == false) { //Write the values.... _val = val; //... //... //... //and then modify the write value function WriteValInternal = AfterFreeze; Freeze(); } else { throw new InvalidOperationException(); } } } private void AfterFreeze(object val) { throw new InvalidOperationException(); } public void WriteValue(Object val) { WriteValInternal(val); } public Object ReadSomething() { return _val; } } 

you may achieve this using POST Sharp

take one interface

 public interface IPseudoImmutable { bool IsFrozen { get; } bool Freeze(); } 

then derive your attribute from InstanceLevelAspect like this

  /// <summary> /// implement by divyang /// </summary> [Serializable] [IntroduceInterface(typeof(IPseudoImmutable), AncestorOverrideAction = InterfaceOverrideAction.Ignore, OverrideAction = InterfaceOverrideAction.Fail)] public class PseudoImmutableAttribute : InstanceLevelAspect, IPseudoImmutable { private volatile bool isFrozen; #region "IPseudoImmutable" [IntroduceMember] public bool IsFrozen { get { return this.isFrozen; } } [IntroduceMember(IsVirtual = true, OverrideAction = MemberOverrideAction.Fail)] public bool Freeze() { if (!this.isFrozen) { this.isFrozen = true; } return this.IsFrozen; } #endregion [OnLocationSetValueAdvice] [MulticastPointcut(Targets = MulticastTargets.Property | MulticastTargets.Field)] public void OnValueChange(LocationInterceptionArgs args) { if (!this.IsFrozen) { args.ProceedSetValue(); } } } public class ImmutableException : Exception { /// <summary> /// The location name. /// </summary> private readonly string locationName; /// <summary> /// Initializes a new instance of the <see cref="ImmutableException"/> class. /// </summary> /// <param name="message"> /// The message. /// </param> public ImmutableException(string message) : base(message) { } public ImmutableException(string message, string locationName) : base(message) { this.locationName = locationName; } public string LocationName { get { return this.locationName; } } } 

then apply in your class like this

  [PseudoImmutableAttribute] public class TestClass { public string MyString { get; set; } public int MyInitval { get; set; } } 

then run it in multi thread

  /// <summary> /// The program. /// </summary> public class Program { /// <summary> /// The main. /// </summary> /// <param name="args"> /// The args. /// </param> public static void Main(string[] args) { Console.Title = "Divyang Demo "; var w = new Worker(); w.Run(); Console.ReadLine(); } } internal class Worker { private object SyncObject = new object(); public Worker() { var r = new Random(); this.ObjectOfMyTestClass = new MyTestClass { MyInitval = r.Next(500) }; } public MyTestClass ObjectOfMyTestClass { get; set; } public void Run() { Task readWork; readWork = Task.Factory.StartNew( action: () => { for (;;) { Task.Delay(1000); try { this.DoReadWork(); } catch (Exception exception) { // Console.SetCursorPosition(80,80); // Console.SetBufferSize(100,100); Console.WriteLine("Read Exception : {0}", exception.Message); } } // ReSharper disable FunctionNeverReturns }); Task writeWork; writeWork = Task.Factory.StartNew( action: () => { for (int i = 0; i < int.MaxValue; i++) { Task.Delay(1000); try { this.DoWriteWork(); } catch (Exception exception) { Console.SetCursorPosition(80, 80); Console.SetBufferSize(100, 100); Console.WriteLine("write Exception : {0}", exception.Message); } if (i == 5000) { ((IPseudoImmutable)this.ObjectOfMyTestClass).Freeze(); } } }); Task.WaitAll(); } /// <summary> /// The do read work. /// </summary> public void DoReadWork() { // ThreadId where reading is done var threadId = System.Threading.Thread.CurrentThread.ManagedThreadId; // printing on screen lock (this.SyncObject) { Console.SetCursorPosition(0, 0); Console.SetBufferSize(290, 290); Console.WriteLine("\n"); Console.WriteLine("Read Start"); Console.WriteLine("Read => Thread Id: {0} ", threadId); Console.WriteLine("Read => this.objectOfMyTestClass.MyInitval: {0} ", this.ObjectOfMyTestClass.MyInitval); Console.WriteLine("Read => this.objectOfMyTestClass.MyString: {0} ", this.ObjectOfMyTestClass.MyString); Console.WriteLine("Read End"); Console.WriteLine("\n"); } } /// <summary> /// The do write work. /// </summary> public void DoWriteWork() { // ThreadId where reading is done var threadId = System.Threading.Thread.CurrentThread.ManagedThreadId; // random number generator var r = new Random(); var count = r.Next(15); // new value for Int property var tempInt = r.Next(5000); this.ObjectOfMyTestClass.MyInitval = tempInt; // new value for string Property var tempString = "Randome" + r.Next(500).ToString(CultureInfo.InvariantCulture); this.ObjectOfMyTestClass.MyString = tempString; // printing on screen lock (this.SyncObject) { Console.SetBufferSize(290, 290); Console.SetCursorPosition(125, 25); Console.WriteLine("\n"); Console.WriteLine("Write Start"); Console.WriteLine("Write => Thread Id: {0} ", threadId); Console.WriteLine("Write => this.objectOfMyTestClass.MyInitval: {0} and New Value :{1} ", this.ObjectOfMyTestClass.MyInitval, tempInt); Console.WriteLine("Write => this.objectOfMyTestClass.MyString: {0} and New Value :{1} ", this.ObjectOfMyTestClass.MyString, tempString); Console.WriteLine("Write End"); Console.WriteLine("\n"); } } } but still it will allow you to change property like array ,list . but if you apply more login in that then it may work for all type of property and field