Interlocked.CompareExchange如果不相等?

时间:2012-11-25 23:22:52

标签: c# multithreading atomic interlocked

  

可能重复:
  Interlocked.CompareExchange<Int> using GreaterThan or LessThan instead of equality

我知道只有当值和比较相等时,Interlocked.CompareExchange才会交换值 如果他们不等于达到这样的目的,如何交换他们?

if (Interlocked.CompareExchange(ref count, count + 1, max) != max)
    // i want it to increment as long as it is not equal to max
        {
           //count should equal to count + 1
        }

2 个答案:

答案 0 :(得分:6)

更高效(更少的总线锁定和更少的读取)以及Marc发布的简化实现:

static int InterlockedIncrementAndClamp(ref int count, int max)
{
    int oldval = Volatile.Read(ref count), val = ~oldval;

    while(oldval != max && oldval != val)
    {   
        val = oldval;
        oldval = Interlocked.CompareExchange(ref count, oldval + 1, oldval);
    }

    return oldval + 1;
}

如果你有很高的争用,我们可以通过将常见情况减少到单个原子增量指令来进一步提高可伸缩性:与CompareExchange相同的开销,但没有循环的可能性。

static int InterlockedIncrementAndClamp(ref int count, int max, int drift)
{
    int v = Interlocked.Increment(ref count);

    while(v > (max + drift))
    {
        // try to adjust value.
        v = Interlocked.CompareExchange(ref count, max, v);
    }

    return Math.Min(v, max);
}

我们允许count超过drift以上的max值。但我们仍然只返回max。这允许我们在大多数情况下将整个op折叠成单个原子增量,这将允许最大的可伸缩性。如果我们超过drift值,我们只需要一个以上的操作,你可能会把它变得非常稀有。

回应Marc对互锁和非互锁内存访问的担忧:

关于具体volatile vs Interlocked:volatile只是一个普通的内存操作,但是没有被优化掉的一个和一个没有针对其他内存操作重新排序的操作。这个具体问题并不围绕这两个特定属性,所以我们真正谈论的是非互锁与互锁的互操作性。

.NET内存模型保证读取和写入基本整数类型(最多为机器的本机字大小),并且引用是原子的。 Interlocked方法也是原子的。因为.NET只有一个“原子”的定义,所以它们不需要明确地说明它们彼此兼容。

Volatile.Read 保证的一件事是可见性:您将始终获得加载指令,但CPU可能会从其本地缓存中读取旧值而不是新值由不同的CPU放入内存。在大多数情况下,x86不需要担心这一点(MOVNTPS之类的特殊指令是例外),但是对于其他架构来说这是非常可能的事情。

总结一下,这描述了可能影响Volatile.Read的两个问题:首先,我们可以在16位CPU上运行,在这种情况下,读取int不会是原子的我们读到的内容可能不是别人写的价值。其次,即使它是原子的,我们也可能因为可见性而读取旧值。

但影响Volatile.Read并不意味着它们会影响整个算法,这对这些算法是完全安全的。

如果您以非原子方式同时count,第一种情况只会让我们感到困惑。这是因为最终可能发生的事情是(写A [0]; CAS A [0:1];写A [1])。因为我们对count的所有写入都发生在保证原子CAS中,所以这不是问题。当我们只是阅读时,如果我们读错了值,它将被捕获在即将到来的CAS中。

如果您考虑一下,第二种情况实际上只是正常情况下的一种特殊情况,其中值在读取和写入之间发生变化 - 读取只是在我们要求它之前发生。在这种情况下,第一个Interlocked.CompareExchange调用会报告与Volatile.Read给出的值不同的值,并且您将开始循环直到成功。

如果您愿意,可以将Volatile.Read视为低争用案例的纯粹优化。我们可以使用oldval初始化0,它仍然可以正常工作。使用Volatile.Read使得它很有可能只执行一个CAS(正如指令所说的那样,特别是在多CPU配置中非常昂贵)而不是两个。

但是,正如Marc所说 - 有时锁只会更简单!

答案 1 :(得分:4)

没有“比较,如果不相等”,但是:你可以先自己测试一下这个值,然后只在你没有获得线程竞赛时进行更新;这通常意味着如果第二次测试失败,您可能需要循环。在伪代码中:

bool retry;
do {
    retry = false;
    // get current value
    var val = Interlocked.CompareExchange(ref field, 0, 0);
    if(val != max) { // if not maxed
        // increment; if the value isn't what it was above: redo from start
        retry = Interlocked.CompareExchange(ref field, val + 1, val) != val;
    }        
} while (retry);

但坦率地说,锁定会更简单。