线程安全的通用字段

时间:2012-07-18 15:01:50

标签: c# .net thread-safety lockless

我有一个通用字段和一个封装它的属性:

T item;

public T Item
{
    get { return item; }
    set { item = value; }
}

问题是这个属性可以从一个线程写入并同时从多个线程读取。如果Tstructlong,则读者可能会获得部分旧值和部分新值的结果。我该如何防止这种情况?

我尝试使用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);
    }
}

这可行,但在我看来,这是一个过于简单的解决方案,应该是简单的。

性能很重要,因此,如果可能,应避免锁定。

如果setget同时发生,我不关心get是返回旧值还是新值。

3 个答案:

答案 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;
    }
}

}

通过这种方式,您可以确保始终使用引用的类型,并且您的访问权限是无锁的。缺点是所有设置操作都会产生垃圾。