Mar*_*cus 62 .net c# multithreading
我正在设计一个类,我希望在主线程完成配置之后只读它,即"冻结"它.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;
}
}
Run Code Online (Sandbox Code Playgroud)
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 ...
}
}
Run Code Online (Sandbox Code Playgroud)
这里的优点是我们确保发布价值而不会在每次阅读时承受开销.如果在写入_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 ...
}
}
Run Code Online (Sandbox Code Playgroud)
绝对是线程安全的,但是每次读取都必须进行比较交换似乎有点浪费.我知道这个开销可能很小,但我正在寻找一种合理有效的方法(尽管可能就是这样).
尝试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 ...
}
}
Run Code Online (Sandbox Code Playgroud)
但乔·达菲宣称" 说不稳定 ",所以我不认为这是一个解决方案.
尝试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 ...
}
}
Run Code Online (Sandbox Code Playgroud)
同样看起来绝对是线程安全的,但是比使用上面的Interlocked方法有更多的开销,所以我倾向于尝试3而不是这个.
然后我可以提出至少一些(我相信还有更多):
尝试6:使用Thread.VolatileWrite和Thread.VolatileRead,但这些可能有点偏重.
尝试7:使用Thread.MemoryBarrier,似乎有点内部.
尝试8:创建一个不可变的副本 - 不想这样做
总结:
编辑:
也许我的问题不明确,但我特别关注上述尝试为何好或坏的原因.请注意,我在这里谈论的是一个单独的编写器在任何并发读取之前写入然后冻结的情况.我相信尝试1是可以的,但我想确切地知道为什么(因为我想知道是否可以以某种方式优化读取,例如).我不太关心这是否是好的设计实践,而是更关心它的实际线程方面.
非常感谢收到问题的答复,但我选择将此标记为自己的答案,因为我觉得给出的答案并不能完全回答我的问题,我不想给访问该网站的任何人留下印象.答案是正确的,因为它因为赏金到期而被自动标记.此外,我不认为投票数最多的答案是以压倒多数投票的,而不是自动将其标记为答案.
我仍然倾向于尝试#1正确,但是,我会喜欢一些权威的答案.我理解x86有一个强大的模型,但我不想(也不应该)为特定的架构编写代码,毕竟这是关于.NET的好东西之一.
如果您对答案有疑问,请选择其中一种锁定方法,也许使用此处显示的优化方法,以避免对锁定进行大量争用.
小智 25
也许稍微偏离主题但只是出于好奇:)你为什么不使用"真正的"不变性?例如,使Freeze()返回一个不可变的副本(没有"写入方法"或任何其他更改内部状态的可能性)并使用此副本而不是原始对象.您甚至可以在不更改状态的情况下进行操作,并在每次写入操作中返回一个新副本(具有已更改的状态)(afaik字符串类可以正常工作)."真正的不变性"本质上是线程安全的.
我投票尝试5,使用锁(this)实现.
这是使这项工作最可靠的方法.可以使用读写器锁,但收益很少.只需使用普通锁即可.
如有必要,您可以通过先检查_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);
}
}
Run Code Online (Sandbox Code Playgroud)
小智 5
如果您真正创建,填充并冻结对象,然后再将其显示给其他线程,那么您不需要任何特殊处理线程安全性(.NET的强大内存模型已经是您的保证),因此解决方案1是有效.
但是,如果您将未冻结的对象提供给另一个线程(或者如果您在不知道用户将如何使用它的情况下简单地创建您的类),则使用该版本,返回新的完全不可变实例的解决方案可能更好.在这种情况下,Mutable实例类似于StringBuilder,而不可变实例就像字符串一样.如果您需要额外的保证,可变实例可以检查其创建者线程并在从任何其他线程使用时抛出异常(在所有方法中......以避免可能的部分读取).
该东西是否被构造并写入,然后永久冻结并多次读取?
或者你冷冻、解冻、再冷冻多次?
如果是前者,那么也许“被冻结”检查应该在读取器方法中而不是写入器方法中(以防止它在冻结之前读取)。
或者,如果是后者,那么您需要注意的用例是:
在后一种情况下,谷歌显示了许多针对多个读者单个作者的结果,您可能会觉得有趣。