5

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

T item;

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

问题是这个属性可以从一个线程写入并同时从多个线程读取。如果Tstruct, or 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);
    }
}

这会起作用,但在我看来,对于应该简单的事情来说,它是过于复杂的解决方案。

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

如果 aset与 同时发生get,我不在乎get返回旧值还是新值。

4

3 回答 3

3

我最初考虑Interlocked过,但我认为它在这里实际上并没有帮助,因为T它不限于引用类型。(如果是的话,原子性已经很好了。)

老实说,我会锁定开始 - 然后衡量性能。如果锁是非竞争的,它应该是非常便宜的。只有当你证明最简单的解决方案太慢时,才考虑变得更深奥。

基本上,由于此处不受约束的通用性,您对这很简单的期望失败了 - 最有效的实现将根据类型而有所不同。

于 2012-07-18T15:13:02.340 回答
3

这完全取决于类型,T.

如果您能够设置class约束,那么在这种特殊情况下 T您不需要做任何事情。引用分配是原子的。这意味着您不能对基础变量进行部分或损坏的写入。

阅读也是如此。您将无法阅读部分写入的参考。

如果T是一个结构,则只能以原子方式读取/分配以下结构(根据 C# 规范的第 12.5 节,强调我的,也证明了上述陈述):

以下数据类型的读取和写入应是原子的:bool、char、byte、sbyte、short、ushort、uint、int、float 和引用类型。此外,具有上一个列表中的基础类型的枚举类型的读取和写入也应该是原子的。其他类型的读取和写入,包括 long、ulong、double 和 decimal,以及用户定义的类型,不必是原子的。除了为此目的设计的库函数之外,不能保证原子读-修改-写,例如在递增或递减的情况下。

因此,如果您所做的只是尝试读/写,并且满足上述条件之一,那么您不必做任何事情(但这意味着您还必须对 type 施加约束T)。

如果您不能保证对 的约束T,那么您将不得不求助于类似lock语句来同步访问(对于前面提到的读取和写入)。

如果您发现使用该lock语句(实际上是Monitorclass)会降低性能,那么您可以使用SpinLockstructure,因为它旨在帮助在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;当然,鉴于您使用的是简单的分配/读取,它不应该花费长时间(除非您使用的结构由于复制语义而体积庞大)。

当然,您应该自己测试一下您预测将使用此类的情况,并查看哪种方法最适合您(lockSpinLock结构)。

于 2012-07-18T15:20:04.410 回答
-2

为什么你需要保护它呢?

更改变量的引用实例是原子操作。因此,您阅读的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;
    }
}

}

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

于 2012-07-18T15:13:18.923 回答