简体   繁体   English

线程安全的通用字段

[英]Thread-safe generic field

I have a generic field and a property that encapsulates it: 我有一个通用字段和一个封装它的属性:

T item;

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

The problem is that this property can be written to from one thread and read from multiple threads at the same time. 问题是这个属性可以从一个线程写入并同时从多个线程读取。 And if T is a struct , or long , readers might get results that are part old value and part new value. 如果T是一个struct ,或者long ,读者可能得到的结果是旧的价值和新的价值。 How can I prevent that? 我怎么能防止这种情况?

I tried using volatile , but that's not possible: 我尝试使用volatile ,但这是不可能的:

A volatile field cannot be of the type 'T'. 易失性字段不能是“T”类型。

Since this is a simpler case of code I've already written, which uses ConcurrentQueue<T> , I thought about using it here too: 由于这是一个更简单的代码我已经编写过,它使用了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);
    }
}

This would work, but it seems to me that it's overcomplicated solution to something that should be simple. 这可行,但在我看来,这是一个过于简单的解决方案。

Performance is important, so, if possible, locking should be avoided. 性能很重要,因此,如果可能,应避免锁定。

If a set happens at the same time as get , I don't care whether get returns the old value or the new value. 如果一个setget同时发生,我不关心get返回旧值还是新值。

I originally considered Interlocked , but I don't think it actually helps here as T isn't constrained to be a reference type. 我最初认为是Interlocked ,但我不认为它实际上有帮助,因为T不限制为引用类型。 (If it were, the atomicity would be fine already.) (如果是的话,原子性就已经很好了。)

I would honestly start with locking - then measure performance. 老实说,我会锁定开始 - 然后测量性能。 If the lock is uncontended, it should be really cheap. 如果锁是无条件的,它应该非常便宜。 Only consider getting more esoteric when you've proved that the simplest solution is too slow. 当你证明最简单的解决方案太慢时,只考虑更深奥。

Basically your expectation that this is simple fails due to the unconstrained genericity here - the most efficient implementation will differ based on the type. 基本上你期望这很简单,因为这里的无限制通用性 - 最有效的实现将根据类型而有所不同。

It completely depends on the type, T . 它完全取决于类型T

If you are able to place a class constraint on T then you don't need to do anything in this particular case. 如果您能够在T上放置class约束,那么在这种特定情况下您不需要做任何事情 Reference assignments are atomic . 参考分配是原子的 This means that you can't have a partial or corrupted write to the underlying variable. 这意味着您不能对基础变量进行部分或损坏的写入。

Same thing goes for reads. 读取也是如此。 You won't be able to read a reference that is partially written. 您将无法读取部分书写的引用。

If T is a struct, then only the following structures can be read/assigned atomically (according to section 12.5 of the C# specification, emphasis mine, also justifies the above statement): 如果T是一个结构,那么只能以原子方式读取/分配以下结构(根据C#规范的第12.5节,强调我的,也证明了上述声明):

Reads and writes of the following data types shall be atomic: bool, char, byte, sbyte, short, ushort, uint, int, float, and reference types. 以下数据类型的读取和写入应为原子:bool,char,byte,sbyte,short,ushort,uint,int,float和reference类型。 In addition, reads and writes of enum types with an underlying type in the previous list shall also be atomic. 此外,在先前列表中具有基础类型的枚举类型的读取和写入也应该是原子的。 Reads and writes of other types, including long, ulong, double, and decimal, as well as user-defined types, need not be atomic. 其他类型的读写,包括long,ulong,double和decimal,以及用户定义的类型,不一定是原子的。 Aside from the library functions designed for that purpose, there is no guarantee of atomic read-modify-write, such as in the case of increment or decrement. 除了为此目的而设计的库函数之外,不保证原子读 - 修改 - 写,例如在递增或递减的情况下。

So if all you're doing is trying to read/write, and you meet one of the conditions above, then you don't have to do anything (but it means that you also have to place a constraint on the type T ). 因此,如果您所做的只是尝试读/写,并且您满足上述条件之一,那么您不必做任何事情(但这意味着您还必须对类型T设置约束)。

If you can't guarantee the constraint on T , then you'll have to resort to something like the lock statement to synchronize access (for reads and writes as mentioned before). 如果你不能保证对T的约束,那么你将不得不求助于类似lock语句来同步访问(如前所述的读写)。

If you find that using the lock statement (really, the Monitor class ) degrades performance, then you can use the SpinLock structure , as it's meant to help in places where Monitor is too heavy: 如果您发现使用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();
        }
    }
}

However, be careful, as the performance of SpinLock can degrade and will be the same as the Monitor class if the wait is too long ; 但是,要小心,因为SpinLock的性能会降低,如果等待太长,它将与Monitor类相同 ; of course, given that you are using simple assignments/reads, it shouldn't take too long (unless you are using a structure which is just massive in size, due to copy semantics). 当然,鉴于您使用的是简单的赋值/读取,它不应该花费长时间(除非您使用的是一个体积庞大的结构,因为复制语义)。

Of course, you should test this yourself for the situations that you predict that this class will be used and see which approach is best for you ( lock or the SpinLock structure). 当然,您应该自己测试一下您预测将使用此类的情况,并查看哪种方法最适合您( lockSpinLock结构)。

Why do you need to protect this at all? 为什么你需要保护它呢?

Changing the referenced instance of a variable is a atomic operation. 更改引用的变量实例是原子操作。 So what ever you read with get won't be invalid. 所以你用get读到的东西都不会无效。 You can't tell if its the old or the new instance when set is running at the same time. 您无法确定set的旧实例或新实例是否同时运行。 But other then that you should be fine. 但除此之外你应该没事。

Partition I, Section 12.6.6 of the CLI spec states: "A conforming CLI shall guarantee that read and write access to properly aligned memory locations no larger than the native word size is atomic when all the write accesses to a location are the same size." CLI规范第12.6.6节的分区I指出:“符合要求的CLI应保证当对位置的所有写入访问的大小相同时,对不大于本机字大小的正确对齐的内存位置的读写访问权限是原子的。 “。

And as your variable is a reference type it always has the size of the native word. 并且由于您的变量是引用类型,因此它始终具有本机字的大小。 So your result is never invalid if you do something like this: 因此,如果您执行以下操作,您的结果将永远无效:

Private T _item;
public T Item
{
    get
    {
        return _item;
    }

    set
    {
        _item = value
    }
}

Example if you want to stick to the generic stuff and use it for everything. 例如,如果你想坚持使用通用的东西并将它用于一切。 The approach is the use of a carrier helper class. 该方法是使用运营商助手类。 It reduces the performance considerably but it will be lock free. 它大大降低了性能,但它将无锁。

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

} }

This way you can ensure that you always use referenced types and your access is lock-free. 这样,您可以确保始终使用引用的类型,并且您的访问权限是无锁的。 Downside is that all set operations create garbage. 缺点是所有设置操作都会产生垃圾。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM