我正在设计一个我希望在主线程完成配置后只读的类,即“冻结”它。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似乎认为这将是好的在这篇文章中。我知道写有释放语义,但据我了解,这只与排序有关,并不一定意味着所有线程在写入后立即看到该值。任何人都可以确认吗?这意味着这个解决方案不是线程安全的(当然这可能不是唯一的原因)。
尝试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 ...
}
}
有人宣称“ sayonara易变 ”,所以我不会认为这是一个解决方案。
尝试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的好东西之一。
如果对答案有疑问,请参阅其中一种锁定方法,或许可以通过此处显示的优化来避免锁的争用。
发布于 2018-03-16 13:55:18
也许有些话题,但只是出于好奇:)为什么你不使用“真正的”不变性?例如,使Freeze()返回一个不可变的副本(没有“写入方法”或任何其他可能性来更改内部状态),并使用此副本而不是原始对象。甚至可以在不更改状态的情况下进行操作,并在每次写入操作时返回新的副本(使用更改后的状态)(afaik字符串类用于此操作)。“真正的不变性”本质上是线程安全的。
发布于 2018-03-16 15:18:40
我投了尝试5,使用锁(这个)实现。
这是做这项工作最可靠的手段。可以使用读卡器/写卡器锁,但获得很少的收益。只需要使用正常的锁。
如有必要,可以通过先检查_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);
}
}
https://stackoverflow.com/questions/-100003191
复制相似问题