Interlocked.CompareExchange 指令重新编码初始值

Posted

技术标签:

【中文标题】Interlocked.CompareExchange 指令重新编码初始值【英文标题】:Interlocked.CompareExchange instruction reodering of the initialvalue 【发布时间】:2019-05-14 08:56:17 【问题描述】:

我想知道是否可以将以下代码中的初始值重新排序为在计算之后导致未定义的行为。

以下示例取自https://docs.microsoft.com/en-us/dotnet/api/system.threading.interlocked.compareexchange?view=netframework-4.8

public class ThreadSafe

    // Field totalValue contains a running total that can be updated
    // by multiple threads. It must be protected from unsynchronized 
    // access.
    private float totalValue = 0.0F;

    // The Total property returns the running total.
    public float Total  get  return totalValue; 

    // AddToTotal safely adds a value to the running total.
    public float AddToTotal(float addend)
    
        float initialValue, computedValue;
        do
        
            // Save the current running total in a local variable.
            initialValue = totalValue;
            //Do we need a memory barrier here??
            // Add the new value to the running total.
            computedValue = initialValue + addend;

            // CompareExchange compares totalValue to initialValue. If
            // they are not equal, then another thread has updated the
            // running total since this loop started. CompareExchange
            // does not update totalValue. CompareExchange returns the
            // contents of totalValue, which do not equal initialValue,
            // so the loop executes again.
        
        while (initialValue != Interlocked.CompareExchange(ref totalValue, 
            computedValue, initialValue));
        // If no other thread updated the running total, then 
        // totalValue and initialValue are equal when CompareExchange
        // compares them, and computedValue is stored in totalValue.
        // CompareExchange returns the value that was in totalValue
        // before the update, which is equal to initialValue, so the 
        // loop ends.

        // The function returns computedValue, not totalValue, because
        // totalValue could be changed by another thread between
        // the time the loop ends and the function returns.
        return computedValue;
    

在将总值分配给初始值和实际计算之间是否需要内存屏障?

据我目前了解,如果没有障碍,它可以通过删除导致线程安全问题的初始值的方式进行优化,因为可以使用过时的值计算 computedValue,但 CompareExchange 将不再检测到这一点:

    public float AddToTotal(float addend)
    
        float computedValue;
        do
        
            // Add the new value to the running total.
            computedValue = totalValue + addend;

            // CompareExchange compares totalValue to initialValue. If
            // they are not equal, then another thread has updated the
            // running total since this loop started. CompareExchange
            // does not update totalValue. CompareExchange returns the
            // contents of totalValue, which do not equal initialValue,
            // so the loop executes again.
        
        while (totalValue != Interlocked.CompareExchange(ref totalValue, 
            computedValue, totalValue));
        // If no other thread updated the running total, then 
        // totalValue and initialValue are equal when CompareExchange
        // compares them, and computedValue is stored in totalValue.
        // CompareExchange returns the value that was in totalValue
        // before the update, which is equal to initialValue, so the 
        // loop ends.

        // The function returns computedValue, not totalValue, because
        // totalValue could be changed by another thread between
        // the time the loop ends and the function returns.
        return computedValue;
    

这里是否缺少局部变量的特殊规则来解释为什么该示例不使用内存屏障?

【问题讨论】:

第二个例子(没有快照到initialValue)显然是非常错误的,因为它在多个点读取totalValue,因此失去了任何理智的保证;如果是我,我会在顶部使用initialValue = Volatile.Read(ref totalValue);,但是... 是的,第二个例子应该是错误的。问题是优化会导致相同的损坏代码吗?如果是这样,那么微软的例子是错误的。如果这不可能,我很好奇为什么它是正确的。 这不是“优化”,但是 - 这是非常不同的代码,可以进行 多次 读取,而不仅仅是重新排序的一次读取;我看不到 single read 可以重新排序超过它会影响 CEX 的点的方式 如果是这种情况,也不需要 Volatile.Read 或任何内存屏障。如果它使用过时的值,那么比较交换将确保它会使用更新的值重试,直到成功。 我认为大多数时候它会增加一点额外的开销,因为即使它已经在缓存中,它也必须从内存中获取值。只有当它以特定工作负载进行基准测试时,才真正知道什么是更快 【参考方案1】:

CPU 绝不会以可能影响单线程执行逻辑的方式“重新排序”指令。万一

initialValue = totalValue;
computedValue = initialValue + addend;

第二个操作肯定是依赖于前一个操作中设置的值。 CPU 从其单线程逻辑的角度“理解”这一点,因此该序列永远不会被重新排序。但是,可以重新排序以下序列:

initialValue = totalValue;
anotherValue = totalValue;

varToInitialize = someVal;
initialized = true;

正如您所见,单核执行不会受到影响,但在多核上这可能会带来一些问题。例如,如果我们围绕以下事实构建逻辑:如果变量initialized 设置为true,那么varToInitialize 应该用某个值初始化,我们可能会在多核环境中遇到麻烦:

if (initialized)

    var storageForVal = varToInitialize; // can still be not initalized
    ...
    // do something with storageForVal with assumption that we have correct value

至于局部变量。重新排序的问题是全局可见性问题,即一个内核/CPU对其他内核/CPU所做的更改的可见性。局部变量主要倾向于仅由单个线程可见(除了一些罕见的情况,例如在方法外部暴露的情况下有效地不是局部变量的闭包),因此其他线程无法访问它们并且因此其他内核/CPU 不需要它们的全局可见性。所以换句话说,在绝大多数情况下,您无需担心局部变量操作的重新排序。

【讨论】:

但在单线程操作中,将 initialValue 替换为 totalValue 是正确的吗?如果您考虑多个线程,它只会导致错误。 @Barsonax,在这种特定情况下,initialValue 仅用于检测新值是否由于某些较新值而成功存储(与乐观锁定的原理相同)。当然,在单线程环境中,我们不需要这种额外的验证,因为没有其他线程会重写目标值,所以我们也不需要initialValue

以上是关于Interlocked.CompareExchange 指令重新编码初始值的主要内容,如果未能解决你的问题,请参考以下文章