我正在设计一个我希望在主线程完成配置后使其只读的类,即“冻结”它。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;
}
}
Eric Lippert 似乎建议在这篇文章中这样做是可以的。我知道写入具有释放语义,但据我了解,这仅与ordering相关,并不一定意味着所有线程都会在写入后立即看到该值。谁能证实这一点?这意味着这个解决方案不是线程安全的(当然这可能不是唯一的原因)。
尝试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
and Thread.VolatileRead
,但据说这些有点偏重。
尝试 7:使用Thread.MemoryBarrier
,似乎有点太内部了。
尝试 8:创建不可变副本 - 不想这样做
总结:
- 您将使用哪种尝试以及为什么(或者如果完全不同,您将如何做)?(即,发布一个值的最佳方式是什么,然后同时读取它,同时在不过度“聪明”的情况下合理高效?)
- .NET 的内存模型“释放”写入语义是否意味着所有其他线程都看到更新(缓存一致性等)?我一般不想过多思考这个问题,但能有一个了解就好了。
编辑:
也许我的问题并不清楚,但我正在特别寻找上述尝试是好是坏的原因。请注意,我在这里谈论的是一个单一写入器的场景,该写入器写入然后在任何并发读取之前冻结。我相信尝试 1 是可以的,但我想确切地知道为什么(例如,我想知道是否可以以某种方式优化读取)。我不太关心这是否是好的设计实践,而是更关心它的实际线程方面。
非常感谢收到的问题的答复,但我自己选择将其标记为答案,因为我觉得给出的答案并不能完全回答我的问题,我不想给访问该网站的任何人留下标记的印象答案是正确的,因为由于赏金到期,它被自动标记为正确。此外,我认为票数最高的答案并没有被压倒性地投票,不足以自动将其标记为答案。
我仍然倾向于尝试 #1 是正确的,但是,我希望得到一些权威的答案。我知道 x86 有一个强大的模型,但我不想(也不应该)为特定架构编写代码,毕竟这是 .NET 的优点之一。
如果您对答案有疑问,请选择一种锁定方法,或许可以使用此处显示的优化来避免对锁定的大量争用。