我有一个通用字段和一个封装它的属性:
T item;
public T Item
{
get { return item; }
set { item = value; }
}
问题是,这个属性可以从一个线程写入,也可以同时从多个线程读取。如果T
是struct
或long
,读者可能会得到部分旧值和部分新值的结果。我该如何防止这种情况发生?
我尝试使用volatile
,但这是不可能的:
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
是返回旧值还是返回新值。
它完全取决于类型T
。
如果您能够在T
上放置class
约束,那么在这种特殊情况下,您不需要执行任何操作。引用分配是原子的。这意味着不能对基础变量进行部分或损坏的写入。
读书也是如此。你将无法阅读部分撰写的参考资料。
如果T
是一个结构,那么只有以下结构可以被原子地读取/分配(根据C#规范的第12.5节,强调挖掘,也证明了上述语句的合理性):
以下数据类型的读取和写入应为原子类型:bool、char、byte、sbyte、short、ushort、uint、int、float和reference类型此外,使用先前列表中的基础类型也应为原子类型。读取和其他类型的写入,包括long、ulong、double和decimal,如以及用户定义的类型不需要是原子类型。除了图书馆为该目的设计的函数,不能保证原子读-修改-写,例如在递增或递减的情况下。
因此,如果你所做的只是尝试读/写,并且你满足上面的条件之一,那么你不必做任何事情(但这意味着你还必须对类型T
施加约束)。
如果不能保证对T
的约束,那么就必须使用类似lock
语句的方法来同步访问(如前所述用于读写)。
如果您发现使用lock
语句(实际上是Monitor
类)会降低性能,那么您可以使用SpinLock
结构,因为它可以在Monitor
太重的地方提供帮助:
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();
}
}
}
但是,请小心,因为如果等待时间过长,SpinLock
的性能可能会降低,并且与Monitor
类的性能相同;当然,考虑到您使用的是简单的赋值/读取,它不应该花费太长的时间(除非您使用的结构由于复制语义而在大小上只是巨大)。
当然,您应该针对您预测将使用此类的情况自己进行测试,看看哪种方法最适合您(lock
或SpinLock
结构)。
我最初考虑的是Interlocked
,但我认为它在这里实际上没有帮助,因为T
不局限于引用类型。(如果是这样的话,原子性就已经很好了。)
老实说,我会以锁定开始,然后衡量性能。如果这把锁是不受控制的,它应该很便宜。只有当你已经证明最简单的解决方案太慢时,才考虑变得更深奥。
基本上,由于这里不受约束的泛型,您认为这是简单的期望失败了——最有效的实现将根据类型而不同。
为什么需要保护它?
更改变量的引用实例是一个原子操作。因此,您使用get
阅读的内容不会无效。当set
同时运行时,您无法判断它是旧实例还是新实例。但除此之外,你应该没事。
CLI规范的分区I第12.6.6节规定:"当对一个位置的所有写访问都是相同的大小时,合格的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;
}
}
}
通过这种方式,您可以确保始终使用引用类型,并且您的访问是无锁定的。不利的一面是,所有的设置操作都会产生垃圾。