我有一个通用字段和一个封装它的属性:
T item;
public T Item
{
get { return item; }
set { item = value; }
}
问题是这个属性可以从一个线程写入并同时从多个线程读取。如果T
是struct
或long
,则读者可能会获得部分旧值和部分新值的结果。我该如何防止这种情况?
我尝试使用volatile
,但这是不可能的:
易失性字段不能是“T”类型。
由于这是一个更简单的代码我已经编写过,使用ConcurrentQueue<T>
,我想在这里使用它:
ConcurrentQueue<T> item;
public T Item
{
get
{
T result;
item.TryPeek(out result);
return item;
}
set
{
item.TryEnqueue(value);
T ignored;
item.TryDequeue(out ignored);
}
}
这可行,但在我看来,这是一个过于简单的解决方案,应该是简单的。
性能很重要,因此,如果可能,应避免锁定。
如果set
与get
同时发生,我不关心get
是返回旧值还是新值。
答案 0 :(得分:3)
我最初考虑Interlocked
,但我不认为它实际上有帮助,因为T
不限制为引用类型。 (如果是的话,原子性就已经很好了。)
老实说启动锁定 - 然后测量性能。如果锁是无条件的,它应该非常便宜。当你证明最简单的解决方案太慢时,只考虑更深奥。
基本上你期望这很简单,因为这里的无限制通用性 - 最有效的实现将根据类型而有所不同。
答案 1 :(得分:3)
完全取决于类型T
。
如果您能够在class
上设置T
约束,则在此特定情况下您无需执行任何。 Reference assignments are atomic。这意味着您不能对基础变量进行部分或损坏的写入。
读取也是如此。您将无法读取部分书写的参考文献。
如果T
是一个结构,那么只能以原子方式读取/分配以下结构(根据C#规范的第12.5节,强调我的,也证明了上述陈述):
以下数据类型的读写应为原子:bool,char,byte,sbyte,short,ushort,uint,int,float和reference 类型。此外,使用枚举类型读取和写入 上一个列表中的基础类型也应该是原子的。读和 其他类型的写入,包括long,ulong,double和decimal,as 以及用户定义的类型,不必是原子的。除了图书馆 为此目的而设计的功能,不保证原子 read-modify-write,例如在递增或递减的情况下。
因此,如果您所做的只是尝试读/写,并且您满足上述条件之一,那么您不必做任何事情(但这意味着您还必须对类型进行约束) T
)。
如果你不能保证对T
的约束,那么你将不得不诉诸lock
statement之类的东西来同步访问(如前所述的读写)。
如果您发现使用lock
语句(实际上,Monitor
class)会降低性能,那么您可以使用SpinLock
structure,因为它可以帮助{{1}太沉重了:
Monitor
但是,要小心,the performance of SpinLock
can degrade and will be the same as the Monitor
class if the wait is too long;当然,鉴于你使用简单的赋值/读取,它不应该太长(除非你使用的结构只是大量的大小,由于复制语义)。
当然,您应该自己测试一下您预测将使用此类的情况,并了解哪种方法最适合您(T item;
SpinLock sl = new SpinLock();
public T Item
{
get
{
bool lockTaken = false;
try
{
sl.Enter(ref lockTaken);
return item;
}
finally
{
if (lockTaken) sl.Exit();
}
}
set
{
bool lockTaken = false;
try
{
sl.Enter(ref lockTaken);
item = value;
}
finally
{
if (lockTaken) sl.Exit();
}
}
}
或lock
结构)。
答案 2 :(得分:-2)
为什么你需要保护它?
更改引用的变量实例是原子操作。因此,您使用get
阅读的内容不会无效。当set
同时运行时,您无法判断它是旧实例还是新实例。但除此之外你应该没事。
CLI规范第12.6.6节的分区I指出:“符合要求的CLI应保证在对位置的所有写入访问都是对原始字大小不大时,对正确对齐的内存位置的读写访问权限是原子的。大小相同。“
由于您的变量是引用类型,因此它始终具有原生单词的大小。因此,如果您执行以下操作,您的结果将永远无效:
Private T _item;
public T Item
{
get
{
return _item;
}
set
{
_item = value
}
}
示例,如果您想坚持使用泛型并将其用于所有内容。该方法是使用运营商助手类。它会大大降低性能,但它将无锁。
Public Foo
{
Private Carrier<T>
{
T _item
}
Private Carrier<T> _item;
public T Item
{
get
{
Dim Carrier<T> carrier = _item;
return carrier.item;
}
set
{
Dim Carrier<T> carrier = new Carrier<T>();
carrier.item = value;
_item = carrier;
}
}
}
通过这种方式,您可以确保始终使用引用的类型,并且您的访问权限是无锁的。缺点是所有设置操作都会产生垃圾。