我正在设计一个我希望在主线程完成配置后只读的类,即“卡住”它。埃里克·利珀特 (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.VolatileWrite
和 Thread.VolatileRead
,但这些据说有点偏重。尝试 7:使用
Thread.MemoryBarrier
,似乎有点太内向了。尝试 8:创建一个不可变的副本 - 不想这样做
总结:
编辑:
也许我的问题不清楚,但我特别在寻找上述尝试是好是坏的原因。请注意,我在这里谈论的是一个单一写入器的场景,该写入器在任何并发读取之前写入然后卡住。我相信尝试 1 是可以的,但我想确切地知道为什么(例如,我想知道读取是否可以以某种方式进行优化)。
我不太关心这是否是好的设计实践,而更关心它的实际线程方面。
非常感谢收到的问题的答复,但我选择自己将其标记为答案,因为我觉得给出的答案并不能完全回答我的问题,而且我不想给访问该网站的任何人留下这样的印象答案是正确的,因为它会因赏金到期而被自动标记为正确。
此外,我认为票数最高的答案并没有被压倒性地投票支持,还不足以自动将其标记为答案。
我仍然倾向于尝试 #1 是正确的,但是,我希望得到一些权威的答案。我知道 x86 有一个强大的模型,但我不想(也不应该)为特定的体系结构编码,毕竟这是 .NET 的优点之一。
如果您对答案有疑问,请使用其中一种锁定方法,也许使用此处显示的优化来避免对锁定的大量争用。
最佳答案
也许稍微偏离主题,但只是出于好奇:) 为什么不使用“真正的”不变性?例如使 Freeze() 返回一个不可变的副本(没有“写方法”或任何其他改变内部状态的可能性)并使用这个副本而不是原始对象。您甚至可以不更改状态并在每次写入操作时返回一个新副本(具有更改的状态)(因为字符串类可以实现此目的)。 “真正的不变性”本质上是线程安全的。
关于c# - 如何在 .NET 中卡住冰棒(使类不可变),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13691612/