【问题标题】:Interlocked.CompareExchange if not equal? [duplicate]如果不相等,Interlocked.CompareExchange? [复制]
【发布时间】:2012-11-25 23:22:51
【问题描述】:

可能重复:
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
        }

【问题讨论】:

    标签: c# multithreading atomic interlocked


    【解决方案1】:

    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。这允许我们在大多数情况下将整个操作折叠成单个原子增量,这将允许最大的可扩展性。如果我们超过 drift 值,我们只需要一个以上的操作,您可能可以将其设置得足够大,以使其非常稀有。

    针对 Marc 对 Interlocked 和 non-Interlocked 内存访问协同工作的担忧:

    特别是volatile 与互锁: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 视为针对低争用情况的纯粹优化。我们可以用0 初始化oldval,它仍然可以正常工作。使用Volatile.Read 使其很有可能只执行一个 CAS(按照说明,这非常昂贵,尤其是在多 CPU 配置中)而不是两个。

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

    【讨论】:

    • 第一次通过时确保val != oldval
    • 是的,我同意:更好的实现。我仍然有点想用Interlocked 进行第一次阅读以确保应用相同的隔离规则,但这非常可爱
    • 我不认为你是对的 - 与 C 不同,在 C# 中,“volatile”和“atomic”实际上被很好地定义为可以一起工作。
    • 来源?我已经多次阅读规范,虽然我知道两者是如何单独定义的,但我不太了解一起
    • 我要编辑帖子来解释一下,因为我的回复很大!
    【解决方案2】:

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

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

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

    【讨论】:

    • +1 到:“锁会更简单”
    • 用 volatile 代替 Interlocked.CompareExchange(ref field, 0, 0) 怎么样?
    • @user1748906 并没有说volatile 将遵守与Interlocked 相同的隔离/友好性规则。它可能会起作用,但是...我想这取决于您期望有多少冲突,就像我已经说过的那样:坦率地说,在许多情况下,锁会更简单
    • @user1748906 当我们发现我们在线程竞赛中迷失时,Cory 的回答会更好地利用该值,即避免每次迭代重复读取。
    猜你喜欢
    • 2012-10-14
    • 2013-02-10
    • 1970-01-01
    • 1970-01-01
    • 2012-10-24
    • 1970-01-01
    • 1970-01-01
    • 2016-01-05
    • 1970-01-01
    相关资源
    最近更新 更多