3

我对 C# 的理解说(感谢 Jeff Richter 和 Jon Skeet)赋值是“原子的”。当我们混合读取和写入(递增/递减)时不是这样,因此我们需要在 Interlocked 上使用方法。如果只有 Read & assign 这两个操作都是原子的吗?

public class Xyz
{
    private volatile int _lastValue;
    private IList<int> AvailableValues { get; set; }
    private object syncRoot = new object();
    private Random random = new Random();

    //Accessible by multiple threads
    public int GetNextValue() //and return last value once store is exhausted
    {
        //...

        var count = 0;
        var returnValue = 0;

        lock (syncRoot)
        {
            count = AvailableValues.Count;
        }

        if (count == 0)
        {
            //Read... without locking... potential multiple reads
            returnValue = _lastValue;
        }
        else
        {

            var toReturn = random.Next(0, count);

            lock (syncRoot)
            {
                returnValue = AvailableValues[toReturn];
                AvailableValues.RemoveAt(toReturn);
            }
            //potential multiple writes... last writer wins
            _lastValue = returnValue;
         }

        return returnValue;

    }

4

6 回答 6

18

我对 C# 的理解说(感谢 Jeff Richter 和 Jon Skeet)赋值是“原子的”。

赋值通常不是原子的。C# 规范仔细地指出了保证是原子的。见第 5.5 节:

以下数据类型的读写是原子的:bool、char、byte、sbyte、short、ushort、uint、int、float 和引用类型。此外,上一个列表中具有基础类型的枚举类型的读取和写入也是原子的。其他类型的读写,包括 long、ulong、double 和 decimal,以及用户定义的类型,不保证是 atomic

(强调补充。)

如果只有 Read & assign 这两个操作都是原子的吗?

同样,第 5.5 节回答了您的问题:

不能保证原子读-修改-写

于 2009-07-08T15:10:36.360 回答
11

volatile实际上与缓存更相关(在寄存器等中);您知道该值实际上是立即volatile写入/读取内存的(实际上并非总是如此)。这允许不同的线程立即看到彼此的更新。指令重新排序还有其他微妙的问题,但这会变得复杂。

这里需要考虑“原子”的两个含义:

  • 是一个单独的读取原子/写入原子本身(即另一个线程可以得到两个不同的一半Double,产生一个从未实际存在的数字)
  • 是一个读/写对原子/隔离在一起

“本身”取决于值的大小;可以在一次操作中更新吗?读/写对更多地与隔离有关 - 即防止丢失更新。

在您的示例中,两个线程可以读取相同的_lastValue,都进行计算,然后(分别)更新_lastValue。其中一项更新将会丢失。实际上,我希望您在读/写过程lock持续时间内想要一个。

于 2009-07-08T12:39:41.913 回答
5

使用 volatile 关键字不会使访问线程安全,它只是确保变量的读取是从内存中读取的,而不是可能从之前读取的缓存中读取的寄存器中读取。某些架构会进行这种优化,这可能会导致在多个线程写入同一个变量的情况下使用过时的值。

为了正确同步访问,您需要拥有更宽的锁:

public class Xyz
{
    private volatile int _lastValue;
    private IList<int> AvailableValues { get; set; }
    private object syncRoot = new object();
    private Random rand = new Random();

    //Accessible by multiple threads
    public int GetNextValue() //and return last value once store is exhausted
    {
        //...

        lock (syncRoot)
        {
            var count = AvailableValues.Count;
            if(count == 0)
                return _lastValue;

            toReturn = rand.Next(0, count);
            _lastValue = AvailableValues[toReturn];
            AvailableValues.RemoveAt(toReturn);
        }
        return _lastValue;
    }
}

如果性能是一个问题,您可能需要考虑为 AvailableValues 使用 LinkedList,因为它支持 O(1) 删除操作。

于 2009-07-08T12:37:55.533 回答
2

对于 .Net 2.0 及之前的版本,有一个名为ReaderWriterLock的类,它允许您分别阻止写入和读取。可能会有所帮助。

对于 .Net 3.5 及更高版本,请考虑ReaderWriterLockSlim,微软这样描述;

ReaderWriterLockSlim 类似于 ReaderWriterLock,但它简化了递归规则以及升级和降级锁定状态的规则。ReaderWriterLockSlim 避免了许多潜在的死锁情况。此外,ReaderWriterLockSlim 的性能明显优于 ReaderWriterLock。ReaderWriterLockSlim 推荐用于所有新开发。

于 2009-07-08T13:01:12.610 回答
0

不能保证这个(原子性)。

于 2009-07-08T12:35:35.970 回答
0

它们适用于某些类型的链接。在您的情况下,它是一个 int,因此根据 C# 规范它是原子的。但与本主题中的其他人一样,它不能保证您的代码是线程安全的。

于 2009-07-08T12:39:57.513 回答